Welcome to mirror list, hosted at ThFree Co, Russian Federation.

Contact.kt « vcard4android « bitfire « at « java « main « src - github.com/bitfireAT/vcard4android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 7ccf52c12fa7064e34cf3d2dc9febcb0042262dd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
/*
 * Copyright © Ricki Hirner (bitfire web engineering).
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 */

package at.bitfire.vcard4android

import at.bitfire.vcard4android.property.*
import ezvcard.Ezvcard
import ezvcard.VCard
import ezvcard.VCardVersion
import ezvcard.io.text.VCardReader
import ezvcard.io.text.VCardWriter
import ezvcard.parameter.EmailType
import ezvcard.parameter.ImageType
import ezvcard.parameter.TelephoneType
import ezvcard.property.*
import ezvcard.util.PartialDate
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.HashCodeBuilder
import org.apache.commons.lang3.builder.ReflectionToStringBuilder
import java.io.IOException
import java.io.OutputStream
import java.io.Reader
import java.net.URI
import java.net.URISyntaxException
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level

class Contact {

    var uid: String? = null
    var group = false

    /** list of UIDs of group members without urn:uuid prefix (only meaningful if [group] is true) */
    val members = LinkedList<String>()

    var displayName: String? = null
    var prefix: String? = null
    var givenName: String? = null
    var middleName: String? = null
    var familyName: String? = null
    var suffix: String? = null

    var phoneticGivenName: String? = null
    var phoneticMiddleName: String? = null
    var phoneticFamilyName: String? = null

    /** vCard NICKNAME – Android only supports one nickname **/
    var nickName: LabeledProperty<Nickname>? = null

    var organization: Organization? = null
    var jobTitle: String? = null           // vCard TITLE
    var jobDescription: String? = null     // vCard ROLE

    val phoneNumbers = LinkedList<LabeledProperty<Telephone>>()
    val emails = LinkedList<LabeledProperty<Email>>()
    val impps = LinkedList<LabeledProperty<Impp>>()
    val addresses = LinkedList<LabeledProperty<Address>>()
    val categories = LinkedList<String>()
    val urls = LinkedList<LabeledProperty<Url>>()
    val relations = LinkedList<Related>()

    var note: String? = null

    var anniversary: Anniversary? = null
    var birthDay: Birthday? = null
    val customDates = LinkedList<LabeledProperty<DateOrTimeProperty>>()

    var photo: ByteArray? = null

    /** unknown properties in text vCard format */
    var unknownProperties: String? = null


    companion object {

        /** list of all custom scribes (will be registered to readers/writers) **/
        val customScribes = arrayOf(
            AbLabel.Scribe,
            AddressBookServerKind.Scribe,
            AddressBookServerMember.Scribe,
            PhoneticFirstName.Scribe,
            PhoneticMiddleName.Scribe,
            PhoneticLastName.Scribe
        )

        // productID (if set) will be used to generate a PRODID property.
        // You may set this statically from the calling application.
        var productID: String? = null

        const val PROPERTY_SIP = "X-SIP"

        // TEL x-types to store Android types
        val PHONE_TYPE_CALLBACK = TelephoneType.get("x-callback")!!
        val PHONE_TYPE_COMPANY_MAIN = TelephoneType.get("x-company_main")!!
        val PHONE_TYPE_RADIO = TelephoneType.get("x-radio")!!
        val PHONE_TYPE_ASSISTANT = TelephoneType.get("X-assistant")!!
        val PHONE_TYPE_MMS = TelephoneType.get("x-mms")!!

        // EMAIL x-types to store Android types
        val EMAIL_TYPE_MOBILE = EmailType.get("x-mobile")!!

        const val NICKNAME_TYPE_MAIDEN_NAME = "x-maiden-name"
        const val NICKNAME_TYPE_SHORT_NAME = "x-short-name"
        const val NICKNAME_TYPE_INITIALS = "x-initials"
        const val NICKNAME_TYPE_OTHER_NAME = "x-other-name"

        const val URL_TYPE_HOMEPAGE = "x-homepage"
        const val URL_TYPE_BLOG = "x-blog"
        const val URL_TYPE_PROFILE = "x-profile"
        const val URL_TYPE_FTP = "x-ftp"

        const val DATE_PARAMETER_OMIT_YEAR = "X-APPLE-OMIT-YEAR"
        const val DATE_PARAMETER_OMIT_YEAR_DEFAULT = 1604


        /**
         * Parses an InputStream that contains a vCard.
         *
         * @param reader     reader for the input stream containing the vCard (pay attention to the charset)
         * @param downloader will be used to download external resources like contact photos (may be null)
         * @return list of filled Event data objects (may have size 0) – doesn't return null
         * @throws IOException on I/O errors when reading the stream
         * @throws ezvcard.io.CannotParseException when the vCard can't be parsed
         */
        fun fromReader(reader: Reader, downloader: Downloader?): List<Contact>  {
            // create new vCard reader and add custom scribes to tell the reader how to read custom properties
            val vCardReader = VCardReader(reader, VCardVersion.V3_0)        // CardDAV requires vCard 3 or newer
            for (scribe in customScribes)
                vCardReader.scribeIndex.register(scribe)

            val vcards = vCardReader.readAll()
            val contacts = LinkedList<Contact>()
            vcards?.forEach { contacts += fromVCard(it, downloader) }
            return contacts
        }

        private fun fromVCard(vCard: VCard, downloader: Downloader?): Contact {
            val c = Contact()

            // get X-ABLabels
            val labels = vCard.getProperties(AbLabel::class.java)

            fun findAndRemoveLabel(group: String?): String? {
                if (group == null)
                    return null

                for (label in labels) {
                    if (label.group.equals(group, true)) {
                        vCard.removeProperty(label)
                        return label.value
                    }
                }

                return null
            }

            // process standard properties
            val toRemove = LinkedList<VCardProperty>()
            for (prop in vCard.properties) {
                var remove = true
                when (prop) {
                    is Uid -> c.uid = uriToUID(prop.value)

                    is Kind, is AddressBookServerKind -> {
                        val kindProp = prop as Kind
                        c.group = kindProp.isGroup
                    }
                    is Member, is AddressBookServerMember -> {
                        val uriProp = prop as Member
                        uriToUID(uriProp.uri)?.let { c.members += it }
                    }

                    is FormattedName -> c.displayName = prop.value.trim()
                    is PhoneticFirstName -> c.phoneticGivenName = StringUtils.trimToNull(prop.value)
                    is PhoneticMiddleName -> c.phoneticMiddleName = StringUtils.trimToNull(prop.value)
                    is PhoneticLastName -> c.phoneticFamilyName = StringUtils.trimToNull(prop.value)
                    is StructuredName -> {
                        c.prefix = StringUtils.trimToNull(prop.prefixes.joinToString(" "))
                        c.givenName = StringUtils.trimToNull(prop.given)
                        c.middleName = StringUtils.trimToNull(prop.additionalNames.joinToString(" "))
                        c.familyName = StringUtils.trimToNull(prop.family)
                        c.suffix = StringUtils.trimToNull(prop.suffixes.joinToString(" "))
                    }
                    is Nickname -> c.nickName = LabeledProperty(prop, findAndRemoveLabel(prop.group))

                    is Organization -> c.organization = prop
                    is Title -> c.jobTitle = StringUtils.trimToNull(prop.value)
                    is Role -> c.jobDescription = StringUtils.trimToNull(prop.value)

                    is Telephone -> if (!prop.text.isNullOrBlank())
                        c.phoneNumbers += LabeledProperty(prop, findAndRemoveLabel(prop.group))
                    is Email -> if (!prop.value.isNullOrBlank())
                        c.emails += LabeledProperty(prop, findAndRemoveLabel(prop.group))
                    is Impp -> c.impps += LabeledProperty(prop, findAndRemoveLabel(prop.group))
                    is Address -> c.addresses += LabeledProperty(prop, findAndRemoveLabel(prop.group))

                    is Note -> c.note = if (c.note.isNullOrEmpty()) prop.value else "${c.note}\n\n\n${prop.value}"
                    is Url -> c.urls += LabeledProperty(prop, findAndRemoveLabel(prop.group))
                    is Categories -> c.categories.addAll(prop.values)

                    is Birthday -> c.birthDay = checkVCard3PartialDate(prop)
                    is Anniversary -> c.anniversary = checkVCard3PartialDate(prop)

                    is Related -> StringUtils.trimToNull(prop.text)?.let { c.relations += prop }
                    is Photo ->
                        c.photo = prop.data ?: prop.url?.let { url ->
                            downloader?.let {
                                Constants.log.info("Downloading photo from $url")
                                it.download(url, "image/*")
                            }
                        }

                    // remove binary properties because of potential OutOfMemory / TransactionTooLarge exceptions
                    is Logo, is Sound -> { /* remove = true */ }

                    // remove properties that don't apply anymore
                    is ProductId,
                    is Revision,
                    is SortString,
                    is Source -> {
                        /* remove = true */
                    }

                    else -> remove = false      // don't remove unknown properties
                }

                if (remove)
                    toRemove += prop
            }
            toRemove.forEach { vCard.removeProperty(it) }

            // process extended properties
            val extToRemove = LinkedList<RawProperty>()
            for (prop in vCard.extendedProperties) {
                var remove = true
                when (prop.propertyName) {
                    PROPERTY_SIP -> c.impps += LabeledProperty(Impp("sip", prop.value), findAndRemoveLabel(prop.group))
                    else -> remove = false      // don't remove unknown extended properties
                }

                if (remove)
                    extToRemove += prop
            }
            extToRemove.distinct().forEach { vCard.removeExtendedProperty(it.propertyName) }

            if (c.uid == null) {
                Constants.log.warning("Received vCard without UID, generating new one")
                c.uid = UUID.randomUUID().toString()
            }

            // remove properties which
            // - couldn't be parsed (and thus are treated as extended/unknown properties), and
            // - must occur once max.
            arrayOf("ANNIVERSARY", "BDAY", "KIND", "N", "PRODID", "REV", "UID").forEach {
                vCard.removeExtendedProperty(it)
            }

            // store all remaining properties into unknownProperties
            if (vCard.properties.isNotEmpty() || vCard.extendedProperties.isNotEmpty())
                try {
                    val writer = Ezvcard.write(vCard)
                    for (scribe in customScribes)       // unknwown properties may contain custom scribes like an unmatched X-ABLabel
                        writer.register(scribe)
                    c.unknownProperties = writer.go()
                } catch(e: Exception) {
                    Constants.log.log(Level.WARNING, "Couldn't serialize unknown properties, dropping them", e)
                }

            return c
        }

        private fun uriToUID(uriString: String?): String? {
            if (uriString == null)
                return null
            return try {
                val uri = URI(uriString)
                when {
                    uri.scheme == null ->
                        uri.schemeSpecificPart
                    uri.scheme.equals("urn", true) && uri.schemeSpecificPart.startsWith("uuid:", true) ->
                        uri.schemeSpecificPart.substring(5)
                    else ->
                        null
                }
            } catch(e: URISyntaxException) {
                Constants.log.warning("Invalid URI for UID: $uriString")
                uriString
            }
        }

        private fun<T: DateOrTimeProperty> checkVCard3PartialDate(property: T): T {
            property.date?.let { date ->
                property.getParameter(DATE_PARAMETER_OMIT_YEAR)?.let { omitYearStr ->
                    try {
                        val omitYear = Integer.parseInt(omitYearStr)
                        val cal = GregorianCalendar.getInstance()
                        cal.time = date
                        if (cal.get(GregorianCalendar.YEAR) == omitYear) {
                            val partial = PartialDate.builder()
                                    .date(cal.get(GregorianCalendar.DAY_OF_MONTH))
                                    .month(cal.get(GregorianCalendar.MONTH) + 1)
                                    .build()
                            property.partialDate = partial
                        }
                    } catch(e: NumberFormatException) {
                        Constants.log.log(Level.WARNING, "Unparseable $DATE_PARAMETER_OMIT_YEAR")
                    } finally {
                        property.removeParameter(DATE_PARAMETER_OMIT_YEAR)
                    }
                }
            }
            return property
        }

    }


    @Throws(IOException::class)
    fun write(vCardVersion: VCardVersion, groupMethod: GroupMethod, os: OutputStream) {
        var vCard = VCard()
        try {
            unknownProperties?.let { vCard = Ezvcard.parse(unknownProperties).first() }
        } catch (e: Exception) {
            Constants.log.fine("Couldn't parse original vCard with retained properties")
        }

        // UID
        uid?.let { vCard.uid = Uid(it) }
        // PRODID
        productID?.let { vCard.setProductId(it) }

        // group support
        if (group && groupMethod == GroupMethod.GROUP_VCARDS) {
            if (vCardVersion == VCardVersion.V4_0) {
                vCard.kind = Kind.group()
                members.forEach { vCard.members += Member("urn:uuid:$it") }
            } else {    // "vCard4 as vCard3" (Apple-style)
                vCard.setProperty(AddressBookServerKind(Kind.GROUP))
                members.forEach { vCard.addProperty(AddressBookServerMember("urn:uuid:$it")) }
            }
        }

        // FN
        var fn = displayName
        if (fn.isNullOrEmpty())
            organization?.let {
                for (part in it.values) {
                    fn = part
                    if (!fn.isNullOrEmpty())
                        break
                }
            }
        if (fn.isNullOrEmpty())
            nickName?.let { fn = it.property.values.firstOrNull() }
        if (fn.isNullOrEmpty())
            emails.firstOrNull()?.let { fn = it.property.value }
        if (fn.isNullOrEmpty())
            phoneNumbers.firstOrNull()?.let { fn = it.property.text }
        if (fn.isNullOrEmpty())
            fn = uid ?: ""
        vCard.setFormattedName(fn)

        // N
        if (prefix != null || familyName != null || middleName != null || givenName != null || suffix != null) {
            val n = StructuredName()
            prefix?.let { it.split(' ').forEach { singlePrefix -> n.prefixes += singlePrefix } }
            n.given = givenName
            middleName?.let { it.split(' ').forEach { singleName -> n.additionalNames += singleName } }
            n.family = familyName
            suffix?.let { it.split(' ').forEach { singleSuffix -> n.suffixes += singleSuffix } }
            vCard.structuredName = n

        } else if (vCardVersion == VCardVersion.V3_0) {
            // (only) vCard 3 requires N [RFC 2426 3.1.2]
            if (group && groupMethod == GroupMethod.GROUP_VCARDS) {
                val n = StructuredName()
                n.family = fn
                vCard.structuredName = n
            } else
                vCard.structuredName = StructuredName()
        }

        // phonetic names
        phoneticGivenName?.let { vCard.addProperty(PhoneticFirstName(it)) }
        phoneticMiddleName?.let { vCard.addProperty(PhoneticMiddleName(it)) }
        phoneticFamilyName?.let { vCard.addProperty(PhoneticLastName(it)) }

        // ORG, TITLE, ROLE
        organization?.let { vCard.organization = it }
        jobTitle?.let { vCard.addTitle(it) }
        jobDescription?.let { vCard.addRole(it) }

        // will be used to count "itemXX." property groups
        val labelIterator = AtomicInteger()
        // TODO move function outside; make clear that it modifies labeledProperty.property
        fun addLabel(labeledProperty: LabeledProperty<VCardProperty>) {
            labeledProperty.label?.let { label ->
                val group = "group${labelIterator.incrementAndGet()}"
                labeledProperty.property.group = group

                val abLabel = AbLabel(label)
                abLabel.group = group
                vCard.addProperty(abLabel)
            }
        }

        // NICKNAME
        nickName?.let { labeledNickName ->
            vCard.addNickname(labeledNickName.property)
            addLabel(labeledNickName)
        }

        // TEL
        for (labeledPhone in phoneNumbers) {
            vCard.addTelephoneNumber(labeledPhone.property)
            addLabel(labeledPhone)
        }

        // EMAIL
        for (labeledEmail in emails) {
            vCard.addEmail(labeledEmail.property)
            addLabel(labeledEmail)
        }

        // IMPP
        for (labeledImpp in impps) {
            vCard.addImpp(labeledImpp.property)
            addLabel(labeledImpp)
        }

        // ADR
        for (labeledAddress in addresses) {
            val address = labeledAddress.property
            vCard.addAddress(address)
            addLabel(labeledAddress)
        }

        // NOTE
        note?.let { vCard.addNote(it) }

        // URL
        for (labeledUrl in urls) {
            val url = labeledUrl.property
            vCard.addUrl(url)
            addLabel(labeledUrl)
        }

        // CATEGORIES
        if (!categories.isEmpty()) {
            val cat = Categories()
            cat.values.addAll(categories)
            vCard.categories = cat
        }

        // ANNIVERSARY, BDAY
        fun<T: DateOrTimeProperty> dateOrPartialDate(prop: T, generator: (Date) -> T): T? {
            if (vCardVersion == VCardVersion.V4_0 || prop.date != null)
                return prop
            else prop.partialDate?.let { partial ->
                // vCard 3: partial date with month and day, but without year
                if (partial.date != null && partial.month != null) {
                    return if (partial.year != null)
                        // partial date is a complete date
                        prop
                    else {
                        // vCard 3: partial date with month and day, but without year
                        val fakeCal = GregorianCalendar.getInstance()
                        fakeCal.set(DATE_PARAMETER_OMIT_YEAR_DEFAULT, partial.month - 1, partial.date)
                        val fakeProp = generator(fakeCal.time)
                        fakeProp.addParameter(DATE_PARAMETER_OMIT_YEAR, DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString())
                        fakeProp
                    }
                }
            }
            return null
        }
        anniversary?.let { vCard.anniversary = dateOrPartialDate(it) { time -> Anniversary(time, false) } }
        birthDay?.let { vCard.birthday = dateOrPartialDate(it) { time -> Birthday(time, false) } }

        // RELATED
        relations.forEach { vCard.addRelated(it) }

        // PHOTO
        photo?.let { vCard.addPhoto(Photo(photo, ImageType.JPEG)) }

        // REV
        vCard.revision = Revision.now()

        // validate vCard and log results
        val validation = vCard.validate(vCardVersion)
        if (!validation.isEmpty) {
            val msgs = LinkedList<String>()
            for ((key, warnings) in validation)
                msgs += "  * " + key.javaClass.simpleName + " - " + warnings.joinToString(" | ")
            Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(","))
        }

        // generate VCARD
        val writer = Ezvcard
                .write(vCard)
                .version(vCardVersion)
                .versionStrict(false)      // allow vCard4 properties in vCard3s
                .caretEncoding(true)           // enable RFC 6868 support
                .prodId(productID == null)

        // tell the writer how to write custom properties
        for (scribe in customScribes)
            writer.register(scribe)

        return writer .go(os)
    }


    private fun compareFields(): Array<Any?> = arrayOf(
        uid,
        group,
        members,
        displayName, prefix, givenName, middleName, familyName, suffix,
        phoneticGivenName, phoneticMiddleName, phoneticFamilyName,
        nickName,
        organization, jobTitle, jobDescription,
        phoneNumbers, emails, impps, addresses,
        /* categories, */ urls, relations,
        note, anniversary, birthDay,
        photo
        /* unknownProperties */
    )

    override fun hashCode(): Int {
        val builder = HashCodeBuilder(29, 3).append(compareFields())
        return builder.toHashCode()
    }

    override fun equals(other: Any?) =
        if (other is Contact)
            compareFields().contentDeepEquals(other.compareFields())
        else
            false

    override fun toString(): String {
        val builder = ReflectionToStringBuilder(this)
        builder.setExcludeFieldNames("photo")
        return builder.toString()
    }


    interface Downloader {
        fun download(url: String, accepts: String): ByteArray?
    }

}