diff --git a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessorTest.kt b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessorTest.kt index f5efed67..9989f263 100644 --- a/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessorTest.kt +++ b/lib/src/androidTest/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessorTest.kt @@ -12,7 +12,6 @@ import android.content.ContentUris import android.content.ContentValues import android.provider.CalendarContract.ACCOUNT_TYPE_LOCAL import android.provider.CalendarContract.AUTHORITY -import android.provider.CalendarContract.Attendees import android.provider.CalendarContract.Events import android.provider.CalendarContract.ExtendedProperties import androidx.core.content.contentValuesOf @@ -24,9 +23,7 @@ import at.bitfire.ical4android.impl.TestCalendar import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter import at.bitfire.ical4android.util.MiscUtils.closeCompat -import at.bitfire.synctools.icalendar.Css3Color import at.bitfire.synctools.storage.calendar.AndroidCalendar -import at.bitfire.synctools.storage.calendar.AndroidCalendarProvider import at.bitfire.synctools.storage.calendar.AndroidEvent2 import at.bitfire.synctools.test.InitCalendarProviderRule import net.fortuna.ical4j.model.Date @@ -35,22 +32,18 @@ import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.ParameterList import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Language -import net.fortuna.ical4j.model.property.Clazz import net.fortuna.ical4j.model.property.DtEnd import net.fortuna.ical4j.model.property.DtStart import net.fortuna.ical4j.model.property.RecurrenceId -import net.fortuna.ical4j.model.property.Status import net.fortuna.ical4j.model.property.XProperty import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule -import java.net.URI /** * Tests mapping from [at.bitfire.synctools.storage.calendar.EventAndExceptions] to [Event]. @@ -148,51 +141,6 @@ class LegacyAndroidEventProcessorTest { } - @Test - fun testPopulateEvent_Sequence_Int() { - populateEvent(true, asSyncAdapter = true) { - put(AndroidEvent2.COLUMN_SEQUENCE, 5) - }.let { result -> - assertEquals(5, result.sequence) - } - } - - @Test - fun testPopulateEvent_Sequence_Null() { - populateEvent(true, asSyncAdapter = true) { - putNull(AndroidEvent2.COLUMN_SEQUENCE) - }.let { result -> - assertNull(result.sequence) - } - } - - @Test - fun testPopulateEvent_IsOrganizer_False() { - populateEvent(true, asSyncAdapter = true) { - put(Events.IS_ORGANIZER, "0") - }.let { result -> - assertFalse(result.isOrganizer!!) - } - } - - @Test - fun testPopulateEvent_IsOrganizer_Null() { - populateEvent(true, asSyncAdapter = true) { - putNull(Events.IS_ORGANIZER) - }.let { result -> - assertNull(result.isOrganizer) - } - } - - @Test - fun testPopulateEvent_IsOrganizer_True() { - populateEvent(true, asSyncAdapter = true) { - put(Events.IS_ORGANIZER, "1") - }.let { result -> - assertTrue(result.isOrganizer!!) - } - } - @Test fun testPopulateEvent_NonAllDay_NonRecurring() { populateEvent(false) { @@ -344,220 +292,6 @@ class LegacyAndroidEventProcessorTest { } } - @Test - fun testPopulateEvent_Summary() { - populateEvent(true) { - put(Events.TITLE, "Sample Title") - }.let { result -> - assertEquals("Sample Title", result.summary) - } - } - - @Test - fun testPopulateEvent_Location() { - populateEvent(true) { - put(Events.EVENT_LOCATION, "Sample Location") - }.let { result -> - assertEquals("Sample Location", result.location) - } - } - - @Test - fun testPopulateEvent_Url() { - populateEvent(true, - extendedProperties = mapOf(AndroidEvent2.EXTNAME_URL to "https://example.com") - ).let { result -> - assertEquals(URI("https://example.com"), result.url) - } - } - - @Test - fun testPopulateEvent_Description() { - populateEvent(true) { - put(Events.DESCRIPTION, "Sample Description") - }.let { result -> - assertEquals("Sample Description", result.description) - } - } - - @Test - fun testPopulateEvent_Color_FromIndex() { - val provider = AndroidCalendarProvider(testAccount, client) - provider.provideCss3ColorIndices() - populateEvent(true) { - put(Events.EVENT_COLOR_KEY, Css3Color.silver.name) - }.let { result -> - assertEquals(Css3Color.silver, result.color) - } - } - - @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) { - put(Events.STATUS, Events.STATUS_CONFIRMED) - }.let { result -> - assertEquals(Status.VEVENT_CONFIRMED, result.status) - } - } - - @Test - fun testPopulateEvent_Status_Tentative() { - populateEvent(true) { - put(Events.STATUS, Events.STATUS_TENTATIVE) - }.let { result -> - assertEquals(Status.VEVENT_TENTATIVE, result.status) - } - } - - @Test - fun testPopulateEvent_Status_Cancelled() { - populateEvent(true) { - put(Events.STATUS, Events.STATUS_CANCELED) - }.let { result -> - assertEquals(Status.VEVENT_CANCELLED, result.status) - } - } - - @Test - fun testPopulateEvent_Status_None() { - assertNull(populateEvent(true).status) - } - - @Test - fun testPopulateEvent_Availability_Busy() { - populateEvent(true) { - put(Events.AVAILABILITY, Events.AVAILABILITY_BUSY) - }.let { result -> - assertTrue(result.opaque) - } - } - - @Test - fun testPopulateEvent_Availability_Tentative() { - populateEvent(true) { - put(Events.AVAILABILITY, Events.AVAILABILITY_TENTATIVE) - }.let { result -> - assertTrue(result.opaque) - } - } - - @Test - fun testPopulateEvent_Availability_Free() { - populateEvent(true) { - put(Events.AVAILABILITY, Events.AVAILABILITY_FREE) - }.let { result -> - assertFalse(result.opaque) - } - } - - @Test - fun testPopulateEvent_Organizer_NotGroupScheduled() { - assertNull(populateEvent(true).organizer) - } - - @Test - fun testPopulateEvent_Organizer_NotGroupScheduled_ExplicitOrganizer() { - populateEvent(true) { - put(Events.ORGANIZER, "sample@example.com") - }.let { result -> - assertNull(result.organizer) - } - } - - @Test - fun testPopulateEvent_Organizer_GroupScheduled() { - populateEvent(true, insertCallback = { id -> - client.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) - }) - }) { - put(Events.ORGANIZER, "organizer@example.com") - }.let { result -> - assertEquals("mailto:organizer@example.com", result.organizer?.value) - } - } - - @Test - fun testPopulateEvent_Classification_Public() { - populateEvent(true) { - put(Events.ACCESS_LEVEL, Events.ACCESS_PUBLIC) - }.let { result -> - assertEquals(Clazz.PUBLIC, result.classification) - } - } - - @Test - fun testPopulateEvent_Classification_Private() { - populateEvent(true) { - put(Events.ACCESS_LEVEL, Events.ACCESS_PRIVATE) - }.let { result -> - assertEquals(Clazz.PRIVATE, result.classification) - } - } - - @Test - fun testPopulateEvent_Classification_Confidential() { - populateEvent(true) { - put(Events.ACCESS_LEVEL, Events.ACCESS_CONFIDENTIAL) - }.let { result -> - assertEquals(Clazz.CONFIDENTIAL, result.classification) - } - } - - @Test - fun testPopulateEvent_Classification_Confidential_Retained() { - populateEvent(true, - extendedProperties = mapOf(UnknownProperty.CONTENT_ITEM_TYPE to UnknownProperty.toJsonString(Clazz.CONFIDENTIAL)) - ) { - put(Events.ACCESS_LEVEL, Events.ACCESS_DEFAULT) - }.let { result -> - assertEquals(Clazz.CONFIDENTIAL, result.classification) - } - } - - @Test - fun testPopulateEvent_Classification_Default() { - populateEvent(true) { - put(Events.ACCESS_LEVEL, Events.ACCESS_DEFAULT) - }.let { result -> - assertNull(result.classification) - } - } - - @Test - fun testPopulateEvent_Classification_Custom() { - 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) - } - } - - @Test - fun testPopulateEvent_Classification_None() { - populateEvent(true) { - }.let { result -> - assertNull(result.classification) - } - } - @Test fun testPopulateUnknownProperty() { diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt index 619e01e7..180f8029 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/LegacyAndroidEventProcessor.kt @@ -10,39 +10,44 @@ import android.content.ContentValues import android.content.Entity import android.provider.CalendarContract.Attendees import android.provider.CalendarContract.Events -import android.provider.CalendarContract.ExtendedProperties import at.bitfire.ical4android.Event -import at.bitfire.ical4android.UnknownProperty import at.bitfire.ical4android.util.AndroidTimeUtils import at.bitfire.ical4android.util.DateUtils import at.bitfire.ical4android.util.TimeApiExtensions import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime import at.bitfire.synctools.exception.InvalidLocalResourceException -import at.bitfire.synctools.icalendar.Css3Color +import at.bitfire.synctools.mapping.calendar.processor.AccessLevelProcessor import at.bitfire.synctools.mapping.calendar.processor.AndroidEventFieldProcessor import at.bitfire.synctools.mapping.calendar.processor.AttendeesProcessor +import at.bitfire.synctools.mapping.calendar.processor.AvailabilityProcessor +import at.bitfire.synctools.mapping.calendar.processor.CategoriesProcessor +import at.bitfire.synctools.mapping.calendar.processor.ColorProcessor +import at.bitfire.synctools.mapping.calendar.processor.DescriptionProcessor +import at.bitfire.synctools.mapping.calendar.processor.LocationProcessor +import at.bitfire.synctools.mapping.calendar.processor.MutatorsProcessor +import at.bitfire.synctools.mapping.calendar.processor.OrganizerProcessor import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor +import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor +import at.bitfire.synctools.mapping.calendar.processor.StatusProcessor +import at.bitfire.synctools.mapping.calendar.processor.TitleProcessor import at.bitfire.synctools.mapping.calendar.processor.UidProcessor -import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcessor +import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor import at.bitfire.synctools.storage.calendar.EventAndExceptions import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.DateList import net.fortuna.ical4j.model.DateTime import net.fortuna.ical4j.model.TimeZoneRegistryFactory import net.fortuna.ical4j.model.parameter.Value -import net.fortuna.ical4j.model.property.Clazz 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.util.TimeZones -import java.net.URI -import java.net.URISyntaxException import java.time.Duration import java.time.Instant import java.time.Period @@ -70,8 +75,25 @@ class LegacyAndroidEventProcessor( private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() } private val fieldProcessors: Array = arrayOf( + // event row fields + MutatorsProcessor(), // for PRODID UidProcessor(), + TitleProcessor(), + LocationProcessor(), + DescriptionProcessor(), + ColorProcessor(), + AccessLevelProcessor(), + AvailabilityProcessor(), + StatusProcessor(), + // scheduling + SequenceProcessor(), + OrganizerProcessor(), AttendeesProcessor(), + // extended properties + CategoriesProcessor(), + UnknownPropertiesProcessor(), + UrlProcessor(), + // sub-components RemindersProcessor(accountName) ) @@ -88,9 +110,6 @@ class LegacyAndroidEventProcessor( originalAllDay = DateUtils.isDate(to.dtStart), to = to ) - - // post-processing - useRetainedClassification(to) } /** @@ -106,14 +125,6 @@ class LegacyAndroidEventProcessor( val hasAttendees = entity.subValues.any { it.uri == Attendees.CONTENT_URI } populateEventRow(entity.entityValues, groupScheduled = hasAttendees, to = to) - // data rows - for (subValue in entity.subValues) { - val subValues = subValue.values - when (subValue.uri) { - ExtendedProperties.CONTENT_URI -> populateExtended(subValues, to = to) - } - } - // new processors for (processor in fieldProcessors) processor.process(from = entity, main = main, to = to) @@ -122,11 +133,6 @@ class LegacyAndroidEventProcessor( private fun populateEventRow(row: ContentValues, groupScheduled: Boolean, to: Event) { logger.log(Level.FINE, "Read event entity from calender provider", row) - row.getAsString(Events.MUTATORS)?.let { strPackages -> - val packages = strPackages.split(AndroidEvent2.MUTATORS_SEPARATOR).toSet() - to.userAgents.addAll(packages) - } - val allDay = (row.getAsInteger(Events.ALL_DAY) ?: 0) != 0 val tsStart = row.getAsLong(Events.DTSTART) ?: throw InvalidLocalResourceException("Found event without DTSTART") @@ -240,55 +246,6 @@ class LegacyAndroidEventProcessor( logger.log(Level.WARNING, "Couldn't parse recurrence rules, ignoring", e) } - to.sequence = row.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) - to.isOrganizer = row.getAsBoolean(Events.IS_ORGANIZER) - - to.summary = row.getAsString(Events.TITLE) - to.location = row.getAsString(Events.EVENT_LOCATION) - to.description = row.getAsString(Events.DESCRIPTION) - - // color can be specified as RGB value and/or as index key (CSS3 color of AndroidCalendar) - to.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)) { - Events.STATUS_CONFIRMED -> to.status = Status.VEVENT_CONFIRMED - Events.STATUS_TENTATIVE -> to.status = Status.VEVENT_TENTATIVE - Events.STATUS_CANCELED -> to.status = Status.VEVENT_CANCELLED - } - - // availability - to.opaque = row.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE - - // scheduling - if (groupScheduled) { - // ORGANIZER must only be set for group-scheduled events (= events with attendees) - if (row.containsKey(Events.ORGANIZER)) - try { - to.organizer = Organizer(URI("mailto", row.getAsString(Events.ORGANIZER), null)) - } catch (e: URISyntaxException) { - logger.log(Level.WARNING, "Error when creating ORGANIZER mailto URI, ignoring", e) - } - } - - // classification - when (row.getAsInteger(Events.ACCESS_LEVEL)) { - Events.ACCESS_PUBLIC -> to.classification = Clazz.PUBLIC - Events.ACCESS_PRIVATE -> to.classification = Clazz.PRIVATE - Events.ACCESS_CONFIDENTIAL -> to.classification = Clazz.CONFIDENTIAL - } - // exceptions from recurring events row.getAsLong(Events.ORIGINAL_INSTANCE_TIME)?.let { originalInstanceTime -> val originalAllDay = (row.getAsInteger(Events.ORIGINAL_ALL_DAY) ?: 0) != 0 @@ -309,31 +266,6 @@ class LegacyAndroidEventProcessor( } } - private fun populateExtended(row: ContentValues, to: Event) { - val name = row.getAsString(ExtendedProperties.NAME) - val rawValue = row.getAsString(ExtendedProperties.VALUE) - logger.log(Level.FINE, "Read extended property from calender provider", arrayOf(name, rawValue)) - - try { - when (name) { - AndroidEvent2.EXTNAME_CATEGORIES -> - to.categories += rawValue.split(AndroidEvent2.CATEGORIES_SEPARATOR) - - AndroidEvent2.EXTNAME_URL -> - try { - to.url = URI(rawValue) - } catch(_: URISyntaxException) { - logger.warning("Won't process invalid local URL: $rawValue") - } - - UnknownProperty.CONTENT_ITEM_TYPE -> - to.unknownProperties += UnknownProperty.fromJsonString(rawValue) - } - } catch (e: Exception) { - logger.log(Level.WARNING, "Couldn't parse extended property", e) - } - } - private fun populateExceptions(exceptions: List, main: Entity, originalAllDay: Boolean, to: Event) { for (exception in exceptions) { val exceptionEvent = Event() @@ -362,29 +294,10 @@ class LegacyAndroidEventProcessor( } } else /* exceptionEvent.status != Status.VEVENT_CANCELLED */ { - // make sure that all components have the same ORGANIZER [RFC 6638 3.1] - exceptionEvent.organizer = to.organizer - // add exception to list of exceptions to.exceptions += exceptionEvent } } } - private fun useRetainedClassification(event: Event) { - var retainedClazz: Clazz? = null - val it = event.unknownProperties.iterator() - while (it.hasNext()) { - val prop = it.next() - if (prop is Clazz) { - retainedClazz = prop - it.remove() - } - } - - if (event.classification == null) - // no classification, use retained one if possible - event.classification = retainedClazz - } - } \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessor.kt new file mode 100644 index 00000000..13d1c0a5 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessor.kt @@ -0,0 +1,55 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.ExtendedProperties +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.UnknownProperty +import net.fortuna.ical4j.model.property.Clazz +import org.json.JSONException + +class AccessLevelProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + val values = from.entityValues + + // take classification from main row + to.classification = when (values.getAsInteger(Events.ACCESS_LEVEL)) { + Events.ACCESS_PUBLIC -> + Clazz.PUBLIC + + Events.ACCESS_PRIVATE -> + Clazz.PRIVATE + + Events.ACCESS_CONFIDENTIAL -> + Clazz.CONFIDENTIAL + + else /* Events.ACCESS_DEFAULT */ -> + retainedClassification(from) + } + } + + private fun retainedClassification(from: Entity): Clazz? { + val extendedProperties = from.subValues.filter { it.uri == ExtendedProperties.CONTENT_URI }.map { it.values } + val unknownProperties = extendedProperties.filter { it.getAsString(ExtendedProperties.NAME) == UnknownProperty.CONTENT_ITEM_TYPE } + val retainedClassification: Clazz? = unknownProperties.firstNotNullOfOrNull { + val json = it.getAsString(ExtendedProperties.VALUE) + val prop = try { + UnknownProperty.fromJsonString(json) + } catch (_: JSONException) { + // not parseable + null + } + prop as? Clazz + } + + return retainedClassification + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AndroidEventFieldProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AndroidEventFieldProcessor.kt index 83f450ea..8aa80988 100644 --- a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AndroidEventFieldProcessor.kt +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AndroidEventFieldProcessor.kt @@ -30,6 +30,7 @@ interface AndroidEventFieldProcessor { * @param from event from content provider * @param main main event from content provider * @param to destination object where the mapped data are stored + * (no explicit `null` values needed for fields that are not present) */ fun process(from: Entity, main: Entity, to: Event) diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessor.kt new file mode 100644 index 00000000..0442d52f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessor.kt @@ -0,0 +1,19 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event + +class AvailabilityProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.opaque = from.entityValues.getAsInteger(Events.AVAILABILITY) != Events.AVAILABILITY_FREE + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessor.kt new file mode 100644 index 00000000..0dc333f6 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessor.kt @@ -0,0 +1,24 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 + +class CategoriesProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + val extended = from.subValues.filter { it.uri == ExtendedProperties.CONTENT_URI }.map { it.values } + val categories = extended.firstOrNull { it.getAsString(ExtendedProperties.NAME) == AndroidEvent2.EXTNAME_CATEGORIES } + val listValue = categories?.getAsString(ExtendedProperties.VALUE) + if (listValue != null) + to.categories += listValue.split(AndroidEvent2.CATEGORIES_SEPARATOR) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessor.kt new file mode 100644 index 00000000..49d742db --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessor.kt @@ -0,0 +1,37 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import at.bitfire.synctools.icalendar.Css3Color +import java.util.logging.Logger + +class ColorProcessor: AndroidEventFieldProcessor { + + private val logger + get() = Logger.getLogger(javaClass.name) + + override fun process(from: Entity, main: Entity, to: Event) { + val values = from.entityValues + + // color can be specified as RGB value and/or as index key (CSS3 color of AndroidCalendar) + to.color = + values.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 + } + } ?: values.getAsInteger(Events.EVENT_COLOR)?.let { color -> // otherwise, try to find the color name from the value + Css3Color.entries.firstOrNull { it.argb == color } + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessor.kt new file mode 100644 index 00000000..5acb3bb4 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessor.kt @@ -0,0 +1,20 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import at.bitfire.vcard4android.Utils.trimToNull + +class DescriptionProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.description = from.entityValues.getAsString(Events.DESCRIPTION).trimToNull() + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessor.kt new file mode 100644 index 00000000..62d27d5c --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessor.kt @@ -0,0 +1,20 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import at.bitfire.vcard4android.Utils.trimToNull + +class LocationProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.location = from.entityValues.getAsString(Events.EVENT_LOCATION).trimToNull() + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessor.kt new file mode 100644 index 00000000..838b77c9 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessor.kt @@ -0,0 +1,23 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 + +class MutatorsProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + from.entityValues.getAsString(Events.MUTATORS)?.let { strPackages -> + val packages = strPackages.split(AndroidEvent2.MUTATORS_SEPARATOR).toSet() + to.userAgents.addAll(packages) + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessor.kt new file mode 100644 index 00000000..cf72c57f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessor.kt @@ -0,0 +1,41 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import net.fortuna.ical4j.model.property.Organizer +import java.net.URI +import java.net.URISyntaxException +import java.util.logging.Level +import java.util.logging.Logger + +class OrganizerProcessor: AndroidEventFieldProcessor { + + private val logger + get() = Logger.getLogger(javaClass.name) + + override fun process(from: Entity, main: Entity, to: Event) { + // In case of an exception, we're taking ORGANIZER information from the main event and not the exception. See also RFC 6638 3.1. + val values = main.entityValues + + // IS_ORGANIZER helper in Event class (deprecated) + to.isOrganizer = values.getAsBoolean(Events.IS_ORGANIZER) + + // ORGANIZER must only be set for group-scheduled events (= events with attendees) + val hasAttendees = from.subValues.any { it.uri == Attendees.CONTENT_URI } + if (hasAttendees && values.containsKey(Events.ORGANIZER)) + try { + to.organizer = Organizer(URI("mailto", values.getAsString(Events.ORGANIZER), null)) + } catch (e: URISyntaxException) { + logger.log(Level.WARNING, "Error when creating ORGANIZER mailto URI, ignoring", e) + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessor.kt new file mode 100644 index 00000000..7c721806 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessor.kt @@ -0,0 +1,19 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 + +class SequenceProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.sequence = from.entityValues.getAsInteger(AndroidEvent2.COLUMN_SEQUENCE) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessor.kt new file mode 100644 index 00000000..e5b3d647 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessor.kt @@ -0,0 +1,32 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import net.fortuna.ical4j.model.property.Status + +class StatusProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.status = when (from.entityValues.getAsInteger(Events.STATUS)) { + Events.STATUS_CONFIRMED -> + Status.VEVENT_CONFIRMED + + Events.STATUS_TENTATIVE -> + Status.VEVENT_TENTATIVE + + Events.STATUS_CANCELED -> + Status.VEVENT_CANCELLED + + else -> + null + } + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessor.kt new file mode 100644 index 00000000..466fdc1f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessor.kt @@ -0,0 +1,20 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.Events +import at.bitfire.ical4android.Event +import at.bitfire.vcard4android.Utils.trimToNull + +class TitleProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + to.summary = from.entityValues.getAsString(Events.TITLE).trimToNull() + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessor.kt new file mode 100644 index 00000000..ea06f50f --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessor.kt @@ -0,0 +1,51 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.UnknownProperty +import net.fortuna.ical4j.model.Property +import org.json.JSONException +import java.util.logging.Level +import java.util.logging.Logger + +class UnknownPropertiesProcessor: AndroidEventFieldProcessor { + + private val logger: Logger + get() = Logger.getLogger(javaClass.name) + + override fun process(from: Entity, main: Entity, to: Event) { + val extended = from.subValues.filter { it.uri == ExtendedProperties.CONTENT_URI }.map { it.values } + val unknownProperties = extended.filter { it.getAsString(ExtendedProperties.NAME) == UnknownProperty.CONTENT_ITEM_TYPE } + val jsonProperties = unknownProperties.mapNotNull { it.getAsString(ExtendedProperties.VALUE) } + for (json in jsonProperties) + try { + val prop = UnknownProperty.fromJsonString(json) + if (!EXCLUDED.contains(prop.name)) + to.unknownProperties += prop + } catch (e: JSONException) { + logger.log(Level.WARNING, "Couldn't parse unknown properties", e) + } + } + + + companion object { + + /** + * These properties are not restored into the [Event.unknownProperties] list. + * Usually they're used by other processors instead. + * + * In the future, this shouldn't be necessary anymore because when other builders/processors store data, + * they shouldn't use an unknown property, but instead define their own extended property. + */ + val EXCLUDED = arrayOf(Property.CLASS) + + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessor.kt b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessor.kt new file mode 100644 index 00000000..546e3ec6 --- /dev/null +++ b/lib/src/main/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessor.kt @@ -0,0 +1,30 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import java.net.URI +import java.net.URISyntaxException + +class UrlProcessor: AndroidEventFieldProcessor { + + override fun process(from: Entity, main: Entity, to: Event) { + val extended = from.subValues.filter { it.uri == ExtendedProperties.CONTENT_URI }.map { it.values } + val urlRow = extended.firstOrNull { it.getAsString(ExtendedProperties.NAME) == AndroidEvent2.EXTNAME_URL } + val url = urlRow?.getAsString(ExtendedProperties.VALUE) + if (url != null) + to.url = try { + URI(url) + } catch (_: URISyntaxException) { + null + } + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessorTest.kt new file mode 100644 index 00000000..f380ec02 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AccessLevelProcessorTest.kt @@ -0,0 +1,102 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import android.provider.CalendarContract.ExtendedProperties +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.UnknownProperty +import net.fortuna.ical4j.model.property.Clazz +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AccessLevelProcessorTest { + + private val processor = AccessLevelProcessor() + + @Test + fun `No access-level`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.classification) + } + + @Test + fun `No access-level, but retained classification`() { + val result = Event() + val entity = Entity(ContentValues()) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + ExtendedProperties.VALUE to "[\"CLASS\",\"x-other\"]" + )) + processor.process(entity, entity, result) + assertEquals(Clazz("x-other"), result.classification) + } + + @Test + fun `Access-level DEFAULT`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ACCESS_LEVEL to Events.ACCESS_DEFAULT + )) + processor.process(entity, entity, result) + assertNull(result.classification) + } + + @Test + fun `Access-level DEFAULT plus retained classification`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ACCESS_LEVEL to Events.ACCESS_DEFAULT + )) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + ExtendedProperties.VALUE to "[\"CLASS\",\"x-other\"]" + )) + processor.process(entity, entity, result) + assertEquals(Clazz("x-other"), result.classification) + } + + @Test + fun `Access-level PUBLIC`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ACCESS_LEVEL to Events.ACCESS_PUBLIC + )) + processor.process(entity, entity, result) + assertEquals(Clazz.PUBLIC, result.classification) + } + + @Test + fun `Access-level PRIVATE`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ACCESS_LEVEL to Events.ACCESS_PRIVATE + )) + processor.process(entity, entity, result) + assertEquals(Clazz.PRIVATE, result.classification) + } + + @Test + fun `Access-level CONFIDENTIAL`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ACCESS_LEVEL to Events.ACCESS_CONFIDENTIAL + )) + processor.process(entity, entity, result) + assertEquals(Clazz.CONFIDENTIAL, result.classification) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessorTest.kt new file mode 100644 index 00000000..7b7e6f24 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/AvailabilityProcessorTest.kt @@ -0,0 +1,63 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class AvailabilityProcessorTest { + + private val processor = AvailabilityProcessor() + + @Test + fun `No availability`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertTrue(result.opaque) + } + + @Test + fun `Availability BUSY`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.AVAILABILITY to Events.AVAILABILITY_BUSY + )) + processor.process(entity, entity, result) + assertTrue(result.opaque) + } + + @Test + fun `Availability FREE`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.AVAILABILITY to Events.AVAILABILITY_FREE + )) + processor.process(entity, entity, result) + assertFalse(result.opaque) + } + + @Test + fun `Availability TENTATIVE`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.AVAILABILITY to Events.AVAILABILITY_TENTATIVE + )) + processor.process(entity, entity, result) + assertTrue(result.opaque) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessorTest.kt new file mode 100644 index 00000000..e3b00157 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/CategoriesProcessorTest.kt @@ -0,0 +1,46 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CategoriesProcessorTest { + + private val processor = CategoriesProcessor() + + @Test + fun `No categories`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertTrue(result.categories.isEmpty()) + } + + @Test + fun `Multiple categories`() { + val result = Event() + val entity = Entity(ContentValues()) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to AndroidEvent2.EXTNAME_CATEGORIES, + ExtendedProperties.VALUE to "Cat 1\\Cat 2" + )) + processor.process(entity, entity, result) + assertEquals(listOf("Cat 1", "Cat 2"), result.categories) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessorTest.kt new file mode 100644 index 00000000..69da8e56 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/ColorProcessorTest.kt @@ -0,0 +1,54 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.synctools.icalendar.Css3Color +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ColorProcessorTest { + + private val processor = ColorProcessor() + + @Test + fun `No color`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.color) + } + + @Test + fun `Color from index`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.EVENT_COLOR_KEY to Css3Color.silver.name + )) + processor.process(entity, entity, result) + assertEquals(Css3Color.silver, result.color) + } + + @Test + fun `Color from value`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.EVENT_COLOR to Css3Color.silver.argb + )) + processor.process(entity, entity, result) + assertEquals(Css3Color.silver, result.color) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessorTest.kt new file mode 100644 index 00000000..d87d3e38 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/DescriptionProcessorTest.kt @@ -0,0 +1,53 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DescriptionProcessorTest { + + private val processor = DescriptionProcessor() + + @Test + fun `No description`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.description) + } + + @Test + fun `Blank description`() { + val entity = Entity(contentValuesOf( + Events.DESCRIPTION to " " + )) + val result = Event() + processor.process(entity, entity, result) + assertNull(result.description) + } + + @Test + fun `Description with two words`() { + val entity = Entity(contentValuesOf( + Events.DESCRIPTION to "Two words " + )) + val result = Event() + processor.process(entity, entity, result) + assertEquals("Two words", result.description) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessorTest.kt new file mode 100644 index 00000000..365f2c02 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/LocationProcessorTest.kt @@ -0,0 +1,53 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class LocationProcessorTest { + + private val processor = LocationProcessor() + + @Test + fun `No event location`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.location) + } + + @Test + fun `Blank event location`() { + val entity = Entity(contentValuesOf( + Events.EVENT_LOCATION to " " + )) + val result = Event() + processor.process(entity, entity, result) + assertNull(result.location) + } + + @Test + fun `Event location with two words`() { + val entity = Entity(contentValuesOf( + Events.EVENT_LOCATION to "Two words " + )) + val result = Event() + processor.process(entity, entity, result) + assertEquals("Two words", result.location) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessorTest.kt new file mode 100644 index 00000000..4ed02475 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/MutatorsProcessorTest.kt @@ -0,0 +1,43 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class MutatorsProcessorTest { + + private val processor = MutatorsProcessor() + + @Test + fun `No mutators`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertTrue(result.userAgents.isEmpty()) + } + + @Test + fun `Multiple mutators`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.MUTATORS to "com.example.calendar,com.example.another.calendar" + )) + processor.process(entity, entity, result) + assertEquals(listOf("com.example.calendar", "com.example.another.calendar"), result.userAgents) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessorTest.kt new file mode 100644 index 00000000..9f1ed1a7 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/OrganizerProcessorTest.kt @@ -0,0 +1,82 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Attendees +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import net.fortuna.ical4j.model.property.Organizer +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 org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class OrganizerProcessorTest { + + private val processor = OrganizerProcessor() + + @Test + fun `isOrganizer not set`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.isOrganizer) + } + + @Test + fun `isOrganizer is 0`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.IS_ORGANIZER to 0, + )) + processor.process(entity, entity, result) + assertFalse(result.isOrganizer!!) + } + + @Test + fun `isOrganizer is 1`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.IS_ORGANIZER to 1, + )) + processor.process(entity, entity, result) + assertTrue(result.isOrganizer!!) + } + + + @Test + fun `No ORGANIZER for non-group-scheduled event`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ORGANIZER to "organizer@example.com" + )) + processor.process(entity, entity, result) + assertNull(result.organizer) + } + + @Test + fun `ORGANIZER for group-scheduled event`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.ORGANIZER to "organizer@example.com" + )) + entity.addSubValue(Attendees.CONTENT_URI, contentValuesOf( + Attendees.ATTENDEE_EMAIL to "organizer@example.com", + Attendees.ATTENDEE_TYPE to Attendees.RELATIONSHIP_ORGANIZER + )) + processor.process(entity, entity, result) + assertEquals(Organizer("mailto:organizer@example.com"), result.organizer) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessorTest.kt new file mode 100644 index 00000000..3649c685 --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/SequenceProcessorTest.kt @@ -0,0 +1,53 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class SequenceProcessorTest { + + private val processor = SequenceProcessor() + + @Test + fun `No sequence`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.sequence) + } + + @Test + fun `Sequence is 0`() { + val entity = Entity(contentValuesOf( + AndroidEvent2.COLUMN_SEQUENCE to 0 + )) + val result = Event() + processor.process(entity, entity, result) + assertEquals(0, result.sequence) + } + + @Test + fun `Sequence is 1`() { + val entity = Entity(contentValuesOf( + AndroidEvent2.COLUMN_SEQUENCE to 1 + )) + val result = Event() + processor.process(entity, entity, result) + assertEquals(1, result.sequence) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessorTest.kt new file mode 100644 index 00000000..48802dcc --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/StatusProcessorTest.kt @@ -0,0 +1,64 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import net.fortuna.ical4j.model.property.Status +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class StatusProcessorTest { + + private val processor = StatusProcessor() + + @Test + fun `No status`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.status) + } + + @Test + fun `Status CONFIRMED`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.STATUS to Events.STATUS_CONFIRMED + )) + processor.process(entity, entity, result) + assertEquals(Status.VEVENT_CONFIRMED, result.status) + } + + @Test + fun `Status TENTATIVE`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.STATUS to Events.STATUS_TENTATIVE + )) + processor.process(entity, entity, result) + assertEquals(Status.VEVENT_TENTATIVE, result.status) + } + + @Test + fun `Status CANCELLED`() { + val result = Event() + val entity = Entity(contentValuesOf( + Events.STATUS to Events.STATUS_CANCELED + )) + processor.process(entity, entity, result) + assertEquals(Status.VEVENT_CANCELLED, result.status) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessorTest.kt new file mode 100644 index 00000000..6885914e --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/TitleProcessorTest.kt @@ -0,0 +1,53 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.Events +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class TitleProcessorTest { + + private val processor = TitleProcessor() + + @Test + fun `No title`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.summary) + } + + @Test + fun `Blank title`() { + val entity = Entity(contentValuesOf( + Events.TITLE to " " + )) + val result = Event() + processor.process(entity, entity, result) + assertNull(result.summary) + } + + @Test + fun `Title with two words`() { + val entity = Entity(contentValuesOf( + Events.TITLE to "Two words " + )) + val result = Event() + processor.process(entity, entity, result) + assertEquals("Two words", result.summary) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessorTest.kt new file mode 100644 index 00000000..826152ac --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UnknownPropertiesProcessorTest.kt @@ -0,0 +1,61 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.UnknownProperty +import net.fortuna.ical4j.model.parameter.XParameter +import net.fortuna.ical4j.model.property.XProperty +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class UnknownPropertiesProcessorTest { + + private val processor = UnknownPropertiesProcessor() + + @Test + fun `No unknown properties`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertTrue(result.unknownProperties.isEmpty()) + } + + @Test + fun `Three unknown properties, one of them excluded`() { + val result = Event() + val entity = Entity(ContentValues()) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( // used by ClassificationProcessor + ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + ExtendedProperties.VALUE to "[\"CLASS\", \"CONFIDENTIAL\"]" + )) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + ExtendedProperties.VALUE to "[\"X-PROP1\", \"value 1\"]" + )) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to UnknownProperty.CONTENT_ITEM_TYPE, + ExtendedProperties.VALUE to "[\"X-PROP2\", \"value 2\", {\"arg1\": \"arg-value\"}]" + )) + processor.process(entity, entity, result) + assertEquals(listOf( + XProperty("X-PROP1", "value 1"), + XProperty("X-PROP2", "value 2").apply { + parameters.add(XParameter("ARG1", "arg-value")) + }, + ), result.unknownProperties) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessorTest.kt b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessorTest.kt new file mode 100644 index 00000000..0fbab5bb --- /dev/null +++ b/lib/src/test/kotlin/at/bitfire/synctools/mapping/calendar/processor/UrlProcessorTest.kt @@ -0,0 +1,59 @@ +/* + * This file is part of bitfireAT/synctools 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.synctools.mapping.calendar.processor + +import android.content.ContentValues +import android.content.Entity +import android.provider.CalendarContract.ExtendedProperties +import androidx.core.content.contentValuesOf +import at.bitfire.ical4android.Event +import at.bitfire.synctools.storage.calendar.AndroidEvent2 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.net.URI + +@RunWith(RobolectricTestRunner::class) +class UrlProcessorTest { + + private val processor = UrlProcessor() + + @Test + fun `No URL`() { + val result = Event() + val entity = Entity(ContentValues()) + processor.process(entity, entity, result) + assertNull(result.url) + } + + @Test + fun `Invalid URL`() { + val result = Event() + val entity = Entity(ContentValues()) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to AndroidEvent2.EXTNAME_URL, + ExtendedProperties.VALUE to "invalid\\uri" + )) + processor.process(entity, entity, result) + assertNull(result.url) + } + + @Test + fun `Valid URL`() { + val result = Event() + val entity = Entity(ContentValues()) + entity.addSubValue(ExtendedProperties.CONTENT_URI, contentValuesOf( + ExtendedProperties.NAME to AndroidEvent2.EXTNAME_URL, + ExtendedProperties.VALUE to "https://example.com" + )) + processor.process(entity, entity, result) + assertEquals(URI("https://example.com"), result.url) + } + +} \ No newline at end of file