Skip to content

Commit 25404dc

Browse files
authored
Explicit PRODID handling (#19)
* Add `PRODID` parameter to `write` methods * Add explicit product ID to vCard generation * Rename lib project in gradle * Fix tests
1 parent 78cacf8 commit 25404dc

File tree

18 files changed

+196
-94
lines changed

18 files changed

+196
-94
lines changed

lib/src/androidTest/kotlin/at/bitfire/ical4android/EventTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package at.bitfire.ical4android
88

9+
import at.bitfire.ical4android.impl.testProdId
910
import at.bitfire.ical4android.util.DateUtils
1011
import net.fortuna.ical4j.model.Date
1112
import net.fortuna.ical4j.model.DateTime
@@ -76,7 +77,7 @@ class EventTest {
7677
e.alarms += VAlarm(Duration.ofMinutes(-30))
7778
e.attendees += Attendee("mailto:[email protected]")
7879
val baos = ByteArrayOutputStream()
79-
e.write(baos)
80+
e.write(baos, testProdId)
8081
val ical = baos.toString()
8182

8283
assertTrue("BEGIN:VTIMEZONE.+BEGIN:STANDARD.+END:STANDARD.+END:VTIMEZONE".toRegex(RegexOption.DOT_MATCHES_ALL).containsMatchIn(ical))
@@ -141,7 +142,7 @@ class EventTest {
141142
)
142143
}
143144
val baos = ByteArrayOutputStream()
144-
event.write(baos)
145+
event.write(baos, testProdId)
145146
val iCal = baos.toString()
146147
assertTrue(iCal.contains("UID:test1\r\n"))
147148
assertTrue(iCal.contains("DTSTART;TZID=Europe/Berlin:20190117T083000\r\n"))
@@ -250,10 +251,10 @@ class EventTest {
250251
e.alarms += VAlarm(Duration.ofHours(-1))
251252

252253
val os = ByteArrayOutputStream()
253-
e.write(os)
254+
e.write(os, testProdId)
254255
val raw = os.toString(Charsets.UTF_8.name())
255256

256-
assertTrue(raw.contains("PRODID:${ICalendar.prodId.value}"))
257+
assertTrue(raw.contains("PRODID:${testProdId.value}"))
257258
assertTrue(raw.contains("UID:SAMPLEUID"))
258259
assertTrue(raw.contains("DTSTART;TZID=Europe/Berlin:20190101T100000"))
259260
assertTrue(raw.contains("DTSTAMP:"))

lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.content.ContentProviderClient
1111
import android.content.ContentValues
1212
import androidx.test.platform.app.InstrumentationRegistry
1313
import at.bitfire.ical4android.impl.TestJtxCollection
14+
import at.bitfire.ical4android.impl.testProdId
1415
import at.bitfire.ical4android.util.MiscUtils.closeCompat
1516
import at.bitfire.synctools.GrantPermissionOrSkipRule
1617
import at.techbee.jtx.JtxContract
@@ -148,9 +149,10 @@ class JtxCollectionTest {
148149
client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv1)
149150
client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv2)
150151

151-
val ics = collections[0].getICSForCollection()
152+
val ics = collections[0].getICSForCollection(testProdId)
152153
assertTrue(ics.contains(Regex("BEGIN:VCALENDAR(\\n*|\\r*|\\t*|.*)*END:VCALENDAR")))
153-
assertTrue(ics.contains("PRODID:+//IDN bitfire.at//ical4android"))
154+
System.err.println(ics)
155+
assertTrue(ics.contains("PRODID:${testProdId.value}"))
154156
assertTrue(ics.contains("SUMMARY:summary"))
155157
assertTrue(ics.contains("SUMMARY:entry2"))
156158
assertTrue(ics.contains(Regex("BEGIN:VJOURNAL(\\n*|\\r*|\\t*|.*)*END:VJOURNAL")))

lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxICalObjectTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import android.os.ParcelFileDescriptor
1414
import androidx.core.content.pm.PackageInfoCompat
1515
import androidx.test.platform.app.InstrumentationRegistry
1616
import at.bitfire.ical4android.impl.TestJtxCollection
17+
import at.bitfire.ical4android.impl.testProdId
1718
import at.bitfire.ical4android.util.MiscUtils.closeCompat
1819
import at.bitfire.synctools.GrantPermissionOrSkipRule
1920
import at.techbee.jtx.JtxContract
@@ -881,7 +882,7 @@ class JtxICalObjectTest {
881882

882883
val os = ByteArrayOutputStream()
883884

884-
iCalObject[0].write(os)
885+
iCalObject[0].write(os, testProdId)
885886

886887
val iCalOut = ICalendar.fromReader(os.toByteArray().inputStream().reader())
887888

lib/src/androidTest/kotlin/at/bitfire/ical4android/TaskTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
package at.bitfire.ical4android
88

9+
import at.bitfire.ical4android.impl.testProdId
910
import at.bitfire.ical4android.util.DateUtils
1011
import net.fortuna.ical4j.model.Date
1112
import net.fortuna.ical4j.model.DateList
1213
import net.fortuna.ical4j.model.DateTime
1314
import net.fortuna.ical4j.model.Parameter
1415
import net.fortuna.ical4j.model.TimeZone
15-
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
1616
import net.fortuna.ical4j.model.component.VAlarm
1717
import net.fortuna.ical4j.model.parameter.RelType
1818
import net.fortuna.ical4j.model.parameter.Value
@@ -207,10 +207,10 @@ class TaskTest {
207207
t.alarms += alarm
208208

209209
val os = ByteArrayOutputStream()
210-
t.write(os)
210+
t.write(os, testProdId)
211211
val raw = os.toString(Charsets.UTF_8.name())
212212

213-
assertTrue(raw.contains("PRODID:${t.prodId().value}"))
213+
assertTrue(raw.contains("PRODID:${testProdId.value}"))
214214
assertTrue(raw.contains("UID:SAMPLEUID"))
215215
assertTrue(raw.contains("DTSTAMP:"))
216216
assertTrue(raw.contains("DTSTART;TZID=Europe/Berlin:20190101T100000"))
@@ -267,7 +267,7 @@ class TaskTest {
267267

268268
private fun regenerate(t: Task): Task {
269269
val os = ByteArrayOutputStream()
270-
t.write(os)
270+
t.write(os, testProdId)
271271
return Task.tasksFromReader(InputStreamReader(ByteArrayInputStream(os.toByteArray()), Charsets.UTF_8)).first()
272272
}
273273

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.ical4android.impl
8+
9+
import net.fortuna.ical4j.model.property.ProdId
10+
11+
val testProdId = ProdId("bitfireAT/synctools test")

lib/src/androidTest/kotlin/at/bitfire/vcard4android/AndroidContactTest.kt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ import androidx.test.platform.app.InstrumentationRegistry
1717
import androidx.test.rule.GrantPermissionRule
1818
import at.bitfire.synctools.storage.LocalStorageException
1919
import at.bitfire.vcard4android.impl.TestAddressBook
20+
import at.bitfire.vcard4android.impl.testProductId
2021
import at.bitfire.vcard4android.property.XAbDate
2122
import ezvcard.VCardVersion
2223
import ezvcard.property.Address
2324
import ezvcard.property.Birthday
2425
import ezvcard.property.Email
2526
import ezvcard.util.PartialDate
26-
import org.junit.*
27-
import org.junit.Assert.*
27+
import org.junit.Assert.assertArrayEquals
28+
import org.junit.Assert.assertEquals
29+
import org.junit.Assert.assertNotNull
30+
import org.junit.Assert.assertTrue
31+
import org.junit.BeforeClass
32+
import org.junit.ClassRule
33+
import org.junit.Test
2834
import java.io.ByteArrayOutputStream
2935
import java.io.StringReader
3036
import java.time.LocalDate
@@ -206,8 +212,8 @@ class AndroidContactTest {
206212
* So, ADR value components may contain DQUOTE (0x22) and don't have to be encoded as defined in RFC 6868 */
207213

208214
val os = ByteArrayOutputStream()
209-
contact.writeVCard(VCardVersion.V4_0, os)
215+
contact.writeVCard(VCardVersion.V4_0, os, testProductId)
210216
assertTrue(os.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;"))
211217
}
212218

213-
}
219+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* This file is part of bitfireAT/synctools which is released under GPLv3.
3+
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
4+
* SPDX-License-Identifier: GPL-3.0-or-later
5+
*/
6+
7+
package at.bitfire.vcard4android.impl
8+
9+
val testProductId = "bitfireAT/synctools test"

lib/src/main/kotlin/at/bitfire/ical4android/Event.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,16 @@ data class Event(
9797
val unknownProperties: LinkedList<Property> = LinkedList()
9898
) : ICalendar() {
9999

100-
fun write(os: OutputStream) {
100+
/**
101+
* Generates an iCalendar from the Event.
102+
*
103+
* @param os stream that the iCalendar is written to
104+
* @param prodId `PRODID` that identifies the app
105+
*/
106+
fun write(os: OutputStream, prodId: ProdId) {
101107
val ical = Calendar()
102108
ical.properties += Version.VERSION_2_0
103-
ical.properties += prodId()
109+
ical.properties += prodId.withUserAgents(userAgents)
104110

105111
val dtStart = dtStart ?: throw InvalidCalendarException("Won't generate event without start time")
106112

@@ -374,4 +380,5 @@ data class Event(
374380
return e
375381
}
376382
}
383+
377384
}

lib/src/main/kotlin/at/bitfire/ical4android/ICalendar.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,29 @@ open class ICalendar {
5656
get() = Logger.getLogger(ICalendar::class.java.name)
5757

5858
// known iCalendar properties
59+
5960
const val CALENDAR_NAME = "X-WR-CALNAME"
6061
const val CALENDAR_COLOR = "X-APPLE-CALENDAR-COLOR"
6162

63+
64+
// PRODID generation
65+
6266
/**
63-
* Default PRODID used when generating iCalendars. If you want another value, set it
64-
* statically before writing the first iCalendar.
67+
* Extends the given `PRODID` with the user agents (typically calendar app name and version).
68+
* This way the `PRODID` does not only identify the app that actually produces the iCalendar,
69+
* but also the used front-end app, which may be helpful when debugging the iCalendar.
70+
*
71+
* @param userAgents list of involved user agents
72+
* (preferably in `package name/version` format, for instance `com.example.mycalendar/1.0`)
73+
*
74+
* @return original `PRODID` with user agents in parentheses
6575
*/
66-
var prodId = ProdId("+//IDN bitfire.at//ical4android")
67-
68-
fun prodId(userAgents: List<String>): ProdId =
76+
fun ProdId.withUserAgents(userAgents: List<String>) =
6977
if (userAgents.isEmpty())
70-
prodId
78+
this
7179
else
72-
ProdId(prodId.value + " (" + userAgents.joinToString(",") + ")")
80+
ProdId(value + " (${userAgents.joinToString(", ")})")
81+
7382

7483
// parser
7584

@@ -373,6 +382,4 @@ open class ICalendar {
373382
uid = UUID.randomUUID().toString()
374383
}
375384

376-
fun prodId(): ProdId = prodId(userAgents)
377-
378385
}

lib/src/main/kotlin/at/bitfire/ical4android/JtxCollection.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import at.techbee.jtx.JtxContract.asSyncAdapter
1919
import net.fortuna.ical4j.model.Calendar
2020
import net.fortuna.ical4j.model.component.VJournal
2121
import net.fortuna.ical4j.model.component.VToDo
22+
import net.fortuna.ical4j.model.property.ProdId
2223
import net.fortuna.ical4j.model.property.Version
2324
import java.util.LinkedList
2425
import java.util.logging.Level
@@ -243,9 +244,11 @@ open class JtxCollection<out T: JtxICalObject>(val account: Account,
243244

244245

245246
/**
247+
* @param prodId `PRODID` that identifies the app
248+
*
246249
* @return a string with all JtxICalObjects within the collection as iCalendar
247250
*/
248-
fun getICSForCollection(): String {
251+
fun getICSForCollection(prodId: ProdId): String {
249252
client.query(
250253
JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(account),
251254
null,
@@ -257,12 +260,12 @@ open class JtxCollection<out T: JtxICalObject>(val account: Account,
257260

258261
val ical = Calendar()
259262
ical.properties += Version.VERSION_2_0
260-
ical.properties += ICalendar.prodId
263+
ical.properties += prodId
261264

262265
while (cursor?.moveToNext() == true) {
263266
val jtxIcalObject = JtxICalObject(this)
264267
jtxIcalObject.populateFromContentValues(cursor.toValues())
265-
val singleICS = jtxIcalObject.getICalendarFormat()
268+
val singleICS = jtxIcalObject.getICalendarFormat(prodId)
266269
singleICS?.components?.forEach { component ->
267270
if(component is VToDo || component is VJournal)
268271
ical.components += component

0 commit comments

Comments
 (0)