diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 56236e8f1b87d169815d348f24eade1579ff5a1b..26cab5b72114748a0b0b1dc63b9ff73ec4ecfbdc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,4 +15,4 @@ cache:
build:
stage: build
script:
- - ./gradlew build
\ No newline at end of file
+ - ./gradlew build
diff --git a/README.md b/README.md
index 0ce4969a215f42bb57110bc603e03cb59e251e43..06e89f7c1cfd7754b552ea93796e5e9f7268d4e4 100644
--- a/README.md
+++ b/README.md
@@ -11,10 +11,15 @@ It's a framework for
from/into data classes that are compatible with the Android Contacts Provider, and
* accessing the Android Contacts Provider by a unified API.
-It has been primarily developed for [DAVdroid](https://www.davdroid.com).
+It has been primarily developed for [DAVx⁵](https://www.davx5.com).
+
+_This software is not affiliated to, nor has it been authorized, sponsored or otherwise approved
+by Google LLC. Android is a trademark of Google LLC._
Generated KDoc: https://bitfireAT.gitlab.io/vcard4android/dokka/vcard4android/
+Discussion: https://forums.bitfire.at/category/18/libraries
+
## Contact
@@ -26,9 +31,6 @@ Florastraße 27
Email: [play@bitfire.at](mailto:play@bitfire.at) (do not use this)
-For questions, suggestions etc. please use the DAVdroid forum:
-https://www.davdroid.com/forums/
-
## License
diff --git a/build.gradle b/build.gradle
index 1a00df96b6047deaa60ead33d5f8a37c84d9abb7..3a91bc975d55476aac48a8492abcca446f33ca93 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,9 @@
buildscript {
- ext.kotlin_version = '1.3.10'
- ext.dokka_version = '0.9.17'
+ ext.versions = [
+ kotlin: '1.3.61',
+ dokka: '0.10.0'
+ ]
repositories {
google()
@@ -9,9 +11,9 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.2.1'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:$dokka_version"
+ classpath 'com.android.tools.build:gradle:3.5.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
+ classpath "org.jetbrains.dokka:dokka-gradle-plugin:${versions.dokka}"
}
}
@@ -22,16 +24,22 @@ repositories {
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
-apply plugin: 'org.jetbrains.dokka-android'
+apply plugin: 'org.jetbrains.dokka'
android {
- compileSdkVersion 27
- buildToolsVersion '27.1.1'
+ compileSdkVersion 29
+ buildToolsVersion '29.0.2'
defaultConfig {
- minSdkVersion 24
- targetSdkVersion 27
+ minSdkVersion 24
+ targetSdkVersion 29
}
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
buildTypes {
release {
minifyEnabled false
@@ -49,18 +57,26 @@ android {
}
defaultConfig {
- testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ dokka.configuration {
+ sourceLink {
+ url = "https://gitlab.com/bitfireAT/vcard4android/tree/master/"
+ lineSuffix = "#L"
+ }
+ jdkVersion = 7
}
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
- implementation 'org.apache.commons:commons-text:1.1'
+ implementation 'org.apache.commons:commons-text:1.8'
implementation 'commons-io:commons-io:2.6'
// ez-vcard to parse/generate VCards
- api('com.googlecode.ez-vcard:ez-vcard:0.10.4') {
+ api('com.googlecode.ez-vcard:ez-vcard:0.10.5') {
// hCard functionality not needed
exclude group: 'org.jsoup'
exclude group: 'org.freemarker'
@@ -68,8 +84,8 @@ dependencies {
exclude group: 'com.fasterxml.jackson.core'
}
- androidTestImplementation 'com.android.support.test:runner:1.0.2'
- androidTestImplementation 'com.android.support.test:rules:1.0.2'
+ androidTestImplementation 'androidx.test:runner:1.2.0'
+ androidTestImplementation 'androidx.test:rules:1.2.0'
testImplementation 'junit:junit:4.12'
}
diff --git a/gradle.properties b/gradle.properties
index 8bd86f6805108dec87d0be823bdb1384bec8aa19..d9cf55df7c1945850a53322a299a58f4b3229097 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1,2 @@
org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5801d8e8035b2cc3012598ce30a163b41d437654..dcc77281b370672855b0e5f012b67acf3db3e2a6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Tue Aug 23 16:42:17 CEST 2016
+#Wed Apr 17 22:31:47 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip
diff --git a/src/androidTest/java/foundation/e/vcard4android/AndroidAddressBookTest.kt b/src/androidTest/java/foundation/e/vcard4android/AndroidAddressBookTest.kt
index aed4338cf57ab697bc9ee2f7f4eea92b9886e20d..060b7f7fe67792e8b4e34e0865f42747dcc48ee9 100644
--- a/src/androidTest/java/foundation/e/vcard4android/AndroidAddressBookTest.kt
+++ b/src/androidTest/java/foundation/e/vcard4android/AndroidAddressBookTest.kt
@@ -13,8 +13,8 @@ import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentValues
import android.provider.ContactsContract
-import android.support.test.InstrumentationRegistry
-import android.support.test.rule.GrantPermissionRule
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import foundation.e.vcard4android.impl.TestAddressBook
import org.junit.After
import org.junit.Assert.*
@@ -33,14 +33,15 @@ class AndroidAddressBookTest {
@Before
fun connect() {
- val context = InstrumentationRegistry.getContext()
- provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ val context = InstrumentationRegistry.getInstrumentation().context
+ provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
assertNotNull(provider)
}
@After
fun disconnect() {
- provider.release()
+ @Suppress("DEPRECATION")
+ provider.release()
}
diff --git a/src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt b/src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt
index 74edbbb494bd9324ba4d9e28f6014bbfba5000e6..026f17841ea654432f5b7d7bbe5dda39bb0e1677 100644
--- a/src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt
+++ b/src/androidTest/java/foundation/e/vcard4android/AndroidContactTest.kt
@@ -12,13 +12,14 @@ import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.provider.ContactsContract
-import android.support.test.InstrumentationRegistry
-import android.support.test.filters.MediumTest
-import android.support.test.filters.SmallTest
-import android.support.test.rule.GrantPermissionRule
import android.util.Base64
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import foundation.e.vcard4android.impl.TestAddressBook
import ezvcard.VCardVersion
+import ezvcard.parameter.EmailType
import ezvcard.property.Address
import ezvcard.property.Birthday
import ezvcard.property.Email
@@ -45,8 +46,8 @@ class AndroidContactTest {
@Before
fun connect() {
- val context = InstrumentationRegistry.getContext()
- provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ val context = InstrumentationRegistry.getInstrumentation().context
+ provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
assertNotNull(provider)
addressBook = TestAddressBook(testAccount, provider)
@@ -54,6 +55,7 @@ class AndroidContactTest {
@After
fun disconnect() {
+ @Suppress("DEPRECATION")
provider.release()
}
@@ -201,6 +203,50 @@ class AndroidContactTest {
}
}
+ @Test
+ @SmallTest
+ fun testEmailTypes() {
+ val vCard = "BEGIN:VCARD\r\n" +
+ "VERSION:4.0\r\n" +
+ "FN:Test\r\n" +
+ "EMAIL;TYPE=internet;TYPE=work:work@example.com\r\n" +
+ "EMAIL;TYPE=home:home@example.com\r\n" +
+ "EMAIL;TYPE=internet,pref:other1@example.com\r\n" +
+ "EMAIL;TYPE=x400,other:other2@example.com\r\n" +
+ "EMAIL;TYPE=x-mobile:mobile@example.com\r\n" +
+ "END:VCARD\r\n"
+ val contacts = Contact.fromReader(StringReader(vCard), null)
+
+ val dbContact = AndroidContact(addressBook, contacts.first(), null, null)
+ dbContact.add()
+
+ val dbContact2 = addressBook.findContactByID(dbContact.id!!)
+ try {
+ val contact2 = dbContact2.contact!!
+ assertEquals("work@example.com", contact2.emails[0].property.value)
+ assertArrayEquals(arrayOf(EmailType.WORK), contact2.emails[0].property.types.toTypedArray())
+ assertNull(contact2.emails[0].property.pref)
+
+ assertEquals("home@example.com", contact2.emails[1].property.value)
+ assertArrayEquals(arrayOf(EmailType.HOME), contact2.emails[1].property.types.toTypedArray())
+ assertNull(contact2.emails[1].property.pref)
+
+ assertEquals("other1@example.com", contact2.emails[2].property.value)
+ assertTrue(contact2.emails[2].property.types.isEmpty())
+ assertNotEquals(0, contact2.emails[2].property.pref)
+
+ assertEquals("other2@example.com", contact2.emails[3].property.value)
+ assertTrue(contact2.emails[3].property.types.isEmpty())
+ assertNull(contact2.emails[3].property.pref)
+
+ assertEquals("mobile@example.com", contact2.emails[4].property.value)
+ assertArrayEquals(arrayOf(Contact.EMAIL_TYPE_MOBILE), contact2.emails[4].property.types.toTypedArray())
+ assertNull(contact2.emails[4].property.pref)
+ } finally {
+ dbContact2.delete()
+ }
+ }
+
@Test
fun testLabelToXName() {
diff --git a/src/androidTest/java/foundation/e/vcard4android/AndroidGroupTest.kt b/src/androidTest/java/foundation/e/vcard4android/AndroidGroupTest.kt
index 7919c5251cc5573c46155d319ad95a48dc71f601..d7a41e210e72ad33b3349bd0df0151c83d1cfcd2 100644
--- a/src/androidTest/java/foundation/e/vcard4android/AndroidGroupTest.kt
+++ b/src/androidTest/java/foundation/e/vcard4android/AndroidGroupTest.kt
@@ -12,8 +12,8 @@ import android.Manifest
import android.accounts.Account
import android.content.ContentProviderClient
import android.provider.ContactsContract
-import android.support.test.InstrumentationRegistry
-import android.support.test.rule.GrantPermissionRule
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import foundation.e.vcard4android.impl.TestAddressBook
import org.junit.After
import org.junit.Assert.assertEquals
@@ -35,8 +35,8 @@ class AndroidGroupTest {
@Before
fun connect() {
- val context = InstrumentationRegistry.getContext()
- provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
+ val context = InstrumentationRegistry.getInstrumentation().context
+ provider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)!!
assertNotNull(provider)
addressBook = TestAddressBook(testAccount, provider)
@@ -44,6 +44,7 @@ class AndroidGroupTest {
@After
fun disconnect() {
+ @Suppress("DEPRECATION")
provider.release()
}
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 03e94cf866acb9efea1d19b717845c176e796e10..24b583a0df5a07b86ea4ab51ae8538dc45efebe1 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -12,7 +12,6 @@
-
+
diff --git a/src/main/java/foundation/e/vcard4android/AndroidAddressBook.kt b/src/main/java/foundation/e/vcard4android/AndroidAddressBook.kt
index 5a8981e25edc86140eefbcfa96e74a489b9cb374..ff4b883392e8b76e729939b6f13894b3524921cc 100644
--- a/src/main/java/foundation/e/vcard4android/AndroidAddressBook.kt
+++ b/src/main/java/foundation/e/vcard4android/AndroidAddressBook.kt
@@ -26,6 +26,8 @@ open class AndroidAddressBook(
protected val groupFactory: AndroidGroupFactory
) {
+ open var readOnly: Boolean = false
+
var settings: ContentValues
/**
* Retrieves [ContactsContract.Settings] for the current address book.
diff --git a/src/main/java/foundation/e/vcard4android/AndroidContact.kt b/src/main/java/foundation/e/vcard4android/AndroidContact.kt
index 1b15d08eee440bc83d517d53f14d315dbb28e0c4..6ed20bdcec0e321c1945a9e56721e76f05869ff1 100644
--- a/src/main/java/foundation/e/vcard4android/AndroidContact.kt
+++ b/src/main/java/foundation/e/vcard4android/AndroidContact.kt
@@ -17,7 +17,6 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.os.RemoteException
import android.provider.ContactsContract
-import android.provider.ContactsContract.CommonDataKinds
import android.provider.ContactsContract.CommonDataKinds.*
import android.provider.ContactsContract.CommonDataKinds.Email
import android.provider.ContactsContract.CommonDataKinds.Nickname
@@ -26,13 +25,14 @@ import android.provider.ContactsContract.CommonDataKinds.Organization
import android.provider.ContactsContract.CommonDataKinds.Photo
import android.provider.ContactsContract.CommonDataKinds.StructuredName
import android.provider.ContactsContract.RawContacts
+import android.provider.ContactsContract.RawContacts.Data
import ezvcard.parameter.*
import ezvcard.property.*
import ezvcard.util.PartialDate
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.builder.ToStringBuilder
-import org.apache.commons.lang3.text.WordUtils
+import org.apache.commons.text.WordUtils
import java.io.ByteArrayOutputStream
import java.io.FileNotFoundException
import java.io.IOException
@@ -40,6 +40,7 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.logging.Level
+import kotlin.math.min
open class AndroidContact(
val addressBook: AndroidAddressBook
@@ -114,7 +115,7 @@ open class AndroidContact(
try {
iter = RawContacts.newEntityIterator(addressBook.provider!!.query(
addressBook.syncAdapterURI(ContactsContract.RawContactsEntity.CONTENT_URI),
- null, ContactsContract.RawContacts._ID + "=?", arrayOf(id.toString()), null))
+ null, RawContacts._ID + "=?", arrayOf(id.toString()), null))
if (iter.hasNext()) {
val e = iter.next()
@@ -134,8 +135,7 @@ open class AndroidContact(
it.remove()
}
- val mimeType = values.getAsString(ContactsContract.RawContactsEntity.MIMETYPE)
- when (mimeType) {
+ when (val mimeType = values.getAsString(ContactsContract.RawContactsEntity.MIMETYPE)) {
StructuredName.CONTENT_ITEM_TYPE ->
populateStructuredName(values)
Phone.CONTENT_ITEM_TYPE ->
@@ -251,13 +251,13 @@ open class AndroidContact(
Phone.TYPE_MMS ->
number.types += Contact.PHONE_TYPE_MMS
Phone.TYPE_CUSTOM -> {
- row.getAsString(CommonDataKinds.Phone.LABEL)?.let {
+ row.getAsString(Phone.LABEL)?.let {
labeledNumber.label = it
number.types += TelephoneType.get(labelToXName(it))
}
}
}
- if (row.getAsInteger(CommonDataKinds.Phone.IS_PRIMARY) != 0)
+ if (row.getAsInteger(Phone.IS_PRIMARY) != 0)
number.pref = 1
contact!!.phoneNumbers += labeledNumber
@@ -453,7 +453,7 @@ open class AndroidContact(
}
protected open fun populateEvent(row: ContentValues) {
- val dateStr = row.getAsString(CommonDataKinds.Event.START_DATE)
+ val dateStr = row.getAsString(Event.START_DATE)
var full: Date? = null
var partial: PartialDate? = null
val fullFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
@@ -572,14 +572,31 @@ open class AndroidContact(
buildContact(builder, true)
batch.enqueue(BatchOperation.Operation(builder))
- // delete known data rows before adding the new ones; don't delete group memberships!
+ // Delete known data rows before adding the new ones.
+ // - We don't delete group memberships.
+ // - We'll only delete rows we have inserted so that unknown rows like
+ // vnd.android.cursor.item/important_people (= contact is in Samsung "edge panel") remain untouched.
batch.enqueue(BatchOperation.Operation(
ContentProviderOperation.newDelete(dataSyncURI())
- .withSelection(RawContacts.Data.RAW_CONTACT_ID + "=? AND " + RawContacts.Data.MIMETYPE + " NOT IN (?,?)",
- arrayOf(id.toString(), GroupMembership.CONTENT_ITEM_TYPE, CachedGroupMembership.CONTENT_ITEM_TYPE))
+ .withSelection(Data.RAW_CONTACT_ID + "=? AND " +
+ Data.MIMETYPE + " IN (?,?,?,?,?,?,?,?,?,?,?,?,?)",
+ arrayOf(id.toString(),
+ StructuredName.CONTENT_ITEM_TYPE,
+ Phone.CONTENT_ITEM_TYPE,
+ Email.CONTENT_ITEM_TYPE,
+ Photo.CONTENT_ITEM_TYPE,
+ Organization.CONTENT_ITEM_TYPE,
+ Im.CONTENT_ITEM_TYPE,
+ Nickname.CONTENT_ITEM_TYPE,
+ Note.CONTENT_ITEM_TYPE,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ Website.CONTENT_ITEM_TYPE,
+ Event.CONTENT_ITEM_TYPE,
+ Relation.CONTENT_ITEM_TYPE,
+ SipAddress.CONTENT_ITEM_TYPE))
))
insertDataRows(batch)
- val results = batch.commit()
+ batch.commit()
insertPhoto(contact.photo)
@@ -600,6 +617,9 @@ open class AndroidContact(
.withValue(COLUMN_ETAG, eTag)
.withValue(COLUMN_UID, contact!!.uid)
+ if (addressBook.readOnly)
+ builder.withValue(RawContacts.RAW_CONTACT_IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built RawContact data row", builder.build())
}
@@ -648,7 +668,7 @@ open class AndroidContact(
op = BatchOperation.Operation(builder)
builder.withValue(StructuredName.RAW_CONTACT_ID, id)
}
- builder .withValue(RawContacts.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ builder .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.DISPLAY_NAME, contact.displayName)
.withValue(StructuredName.PREFIX, contact.prefix)
.withValue(StructuredName.GIVEN_NAME, contact.givenName)
@@ -658,6 +678,10 @@ open class AndroidContact(
.withValue(StructuredName.PHONETIC_GIVEN_NAME, contact.phoneticGivenName)
.withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.phoneticMiddleName)
.withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.phoneticFamilyName)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built StructuredName data row", builder.build())
batch.enqueue(op)
}
@@ -674,9 +698,9 @@ open class AndroidContact(
} catch(e: IllegalStateException) {
Constants.log.log(Level.FINER, "Can't understand phone number PREF", e)
}
- var is_primary = pref != null
+ var isPrimary = pref != null
if (types.contains(TelephoneType.PREF)) {
- is_primary = true
+ isPrimary = true
types -= TelephoneType.PREF
}
@@ -725,6 +749,7 @@ open class AndroidContact(
types.contains(Contact.PHONE_TYPE_MMS) ->
typeCode = Phone.TYPE_MMS
+ types.contains(Contact.PHONE_TYPE_OTHER) ||
types.contains(TelephoneType.VOICE) ||
types.contains(TelephoneType.TEXT) -> {}
@@ -748,8 +773,12 @@ open class AndroidContact(
.withValue(Phone.NUMBER, number.text)
.withValue(Phone.TYPE, typeCode)
.withValue(Phone.LABEL, typeLabel)
- .withValue(Phone.IS_PRIMARY, if (is_primary) 1 else 0)
- .withValue(Phone.IS_SUPER_PRIMARY, if (is_primary) 1 else 0)
+ .withValue(Phone.IS_PRIMARY, if (isPrimary) 1 else 0)
+ .withValue(Phone.IS_SUPER_PRIMARY, if (isPrimary) 1 else 0)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Phone data row", builder.build())
batch.enqueue(op)
}
@@ -757,7 +786,10 @@ open class AndroidContact(
protected open fun insertEmail(batch: BatchOperation, labeledEmail: LabeledProperty) {
val email = labeledEmail.property
+ // drop TYPE=internet and TYPE=x400 because Android only knows Internet email addresses
+ // drop TYPE=other for compatibility, too (non-standard type which is only used by some clients and not useful as an explicit value)
val types = email.types
+ types.removeAll(arrayOf(EmailType.INTERNET, EmailType.X400, Contact.EMAIL_TYPE_OTHER))
// preferred email address?
var pref: Int? = null
@@ -766,9 +798,9 @@ open class AndroidContact(
} catch(e: IllegalStateException) {
Constants.log.log(Level.FINER, "Can't understand email PREF", e)
}
- var is_primary = pref != null
+ var isPrimary = pref != null
if (types.contains(EmailType.PREF)) {
- is_primary = true
+ isPrimary = true
types -= EmailType.PREF
}
@@ -784,7 +816,7 @@ open class AndroidContact(
EmailType.WORK -> typeCode = Email.TYPE_WORK
Contact.EMAIL_TYPE_MOBILE -> typeCode = Email.TYPE_MOBILE
}
- if (typeCode == 0) {
+ if (typeCode == 0) { // we still didn't find a known type
if (email.types.isEmpty())
typeCode = Email.TYPE_OTHER
else {
@@ -806,8 +838,12 @@ open class AndroidContact(
.withValue(Email.ADDRESS, email.value)
.withValue(Email.TYPE, typeCode)
.withValue(Email.LABEL, typeLabel)
- .withValue(Email.IS_PRIMARY, if (is_primary) 1 else 0)
- .withValue(Phone.IS_SUPER_PRIMARY, if (is_primary) 1 else 0)
+ .withValue(Email.IS_PRIMARY, if (isPrimary) 1 else 0)
+ .withValue(Phone.IS_SUPER_PRIMARY, if (isPrimary) 1 else 0)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Email data row", builder.build())
batch.enqueue(op)
}
@@ -841,6 +877,10 @@ open class AndroidContact(
.withValue(Organization.DEPARTMENT, department)
.withValue(Organization.TITLE, contact.jobTitle)
.withValue(Organization.JOB_DESCRIPTION, contact.jobDescription)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Organization data row", builder.build())
batch.enqueue(op)
}
@@ -850,7 +890,6 @@ open class AndroidContact(
var typeCode: Int = Im.TYPE_OTHER
var typeLabel: String? = null
-
if (labeledImpp.label != null) {
typeCode = Im.TYPE_CUSTOM
typeLabel = labeledImpp.label
@@ -905,6 +944,10 @@ open class AndroidContact(
op = BatchOperation.Operation(builder)
builder.withValue(Im.RAW_CONTACT_ID, id)
}
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
if (sipAddress) {
// save as SIP address
builder .withValue(SipAddress.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE)
@@ -958,6 +1001,10 @@ open class AndroidContact(
.withValue(Nickname.NAME, nick.values.first())
.withValue(Nickname.TYPE, typeCode)
.withValue(Nickname.LABEL, typeLabel)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Nickname data row", builder.build())
batch.enqueue(op)
}
@@ -977,6 +1024,10 @@ open class AndroidContact(
}
builder .withValue(Note.MIMETYPE, Note.CONTENT_ITEM_TYPE)
.withValue(Note.NOTE, contact.note)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Note data row", builder.build())
batch.enqueue(op)
}
@@ -998,9 +1049,9 @@ open class AndroidContact(
val lineLocality = arrayOf(address.postalCode, address.locality).filterNot { it.isNullOrEmpty() }.joinToString(" ")
val lines = LinkedList()
- if (!lineStreet.isEmpty())
+ if (lineStreet.isNotEmpty())
lines += lineStreet
- if (!lineLocality.isEmpty())
+ if (lineLocality.isNotEmpty())
lines += lineLocality
if (!address.region.isNullOrEmpty())
lines += address.region
@@ -1010,20 +1061,21 @@ open class AndroidContact(
formattedAddress = lines.joinToString("\n")
}
+ val types = address.types
var typeCode = StructuredPostal.TYPE_OTHER
var typeLabel: String? = null
if (labeledAddress.label != null) {
typeCode = StructuredPostal.TYPE_CUSTOM
typeLabel = labeledAddress.label
} else {
- for (type in address.types)
- when (type) {
- AddressType.HOME -> typeCode = StructuredPostal.TYPE_HOME
- AddressType.WORK -> typeCode = StructuredPostal.TYPE_WORK
+ when {
+ types.contains(AddressType.HOME) -> typeCode = StructuredPostal.TYPE_HOME
+ types.contains(AddressType.WORK) -> typeCode = StructuredPostal.TYPE_WORK
+ types.contains(Contact.ADDRESS_TYPE_OTHER) -> {}
+ types.isNotEmpty() -> {
+ typeCode = StructuredPostal.TYPE_CUSTOM
+ typeLabel = xNameToLabel(address.types.first().value)
}
- if (typeCode == StructuredPostal.TYPE_OTHER && address.types.isNotEmpty()) {
- typeCode = StructuredPostal.TYPE_CUSTOM
- typeLabel = xNameToLabel(address.types.first().value)
}
}
@@ -1046,6 +1098,10 @@ open class AndroidContact(
.withValue(StructuredPostal.REGION, address.region)
.withValue(StructuredPostal.POSTCODE, address.postalCode)
.withValue(StructuredPostal.COUNTRY, address.country)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built StructuredPostal data row", builder.build())
batch.enqueue(op)
}
@@ -1087,6 +1143,10 @@ open class AndroidContact(
.withValue(Website.URL, url.value)
.withValue(Website.TYPE, typeCode)
.withValue(Website.LABEL, typeLabel)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Website data row", builder.build())
batch.enqueue(op)
}
@@ -1117,6 +1177,10 @@ open class AndroidContact(
builder .withValue(Event.MIMETYPE, Event.CONTENT_ITEM_TYPE)
.withValue(Event.TYPE, type)
.withValue(Event.START_DATE, dateStr)
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
batch.enqueue(op)
Constants.log.log(Level.FINER, "Built Event data row", builder.build())
}
@@ -1150,6 +1214,10 @@ open class AndroidContact(
.withValue(Relation.NAME, related.text)
.withValue(Relation.TYPE, typeCode)
.withValue(Relation.LABEL, StringUtils.trimToNull(labels.joinToString(", ")))
+
+ if (addressBook.readOnly)
+ builder.withValue(Data.IS_READ_ONLY, 1)
+
Constants.log.log(Level.FINER, "Built Relation data row", builder.build())
batch.enqueue(op)
}
@@ -1193,7 +1261,7 @@ open class AndroidContact(
if (width > max || height > max) {
val scaleWidth = max/width
val scaleHeight = max/height
- val scale = Math.min(scaleWidth, scaleHeight)
+ val scale = min(scaleWidth, scaleHeight)
val newWidth = (width * scale).toInt()
val newHeight = (height * scale).toInt()
@@ -1217,9 +1285,13 @@ open class AndroidContact(
Constants.log.fine("Inserting photo BLOB for raw contact $id")
val values = ContentValues(3)
- values.put(RawContacts.Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE)
+ values.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE)
values.put(Photo.RAW_CONTACT_ID, id)
values.put(Photo.PHOTO, photo)
+
+ if (addressBook.readOnly)
+ values.put(Data.IS_READ_ONLY, 1)
+
try {
addressBook.provider!!.insert(dataSyncURI(), values)
} catch(e: RemoteException) {
@@ -1246,11 +1318,11 @@ open class AndroidContact(
protected fun rawContactSyncURI(): Uri {
val id = requireNotNull(id)
- return addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.RawContacts.CONTENT_URI, id))
+ return addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id))
}
protected fun dataSyncURI() = addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI)
override fun toString() = ToStringBuilder.reflectionToString(this)!!
-}
+}
\ No newline at end of file
diff --git a/src/main/java/foundation/e/vcard4android/AndroidGroup.kt b/src/main/java/foundation/e/vcard4android/AndroidGroup.kt
index 64de2bb92b3ae2ad890b5542a60e0a0e8d9d66b8..57b6f2de8f7e70134f1668c24ae335b2ce7cfa57 100644
--- a/src/main/java/foundation/e/vcard4android/AndroidGroup.kt
+++ b/src/main/java/foundation/e/vcard4android/AndroidGroup.kt
@@ -113,6 +113,7 @@ open class AndroidGroup(
* Creates a group with data taken from the constructor.
* @return number of affected rows
* @throws RemoteException on contact provider errors
+ * @throws ContactsStorageException when the group can't be added
*/
fun add(): Uri {
val values = contentValues()
@@ -121,6 +122,7 @@ open class AndroidGroup(
values.put(Groups.SHOULD_SYNC, 1)
// read-only: values.put(Groups.GROUP_VISIBLE, 1);
val uri = addressBook.provider!!.insert(addressBook.syncAdapterURI(Groups.CONTENT_URI), values)
+ ?: throw ContactsStorageException("Empty result from content provider when adding group")
id = ContentUris.parseId(uri)
return uri
}
@@ -150,7 +152,7 @@ open class AndroidGroup(
private fun groupSyncURI(): Uri {
val id = requireNotNull(id)
- return addressBook.syncAdapterURI(ContentUris.withAppendedId(ContactsContract.Groups.CONTENT_URI, id))
+ return addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id))
}
override fun toString() = ToStringBuilder.reflectionToString(this)!!
diff --git a/src/main/java/foundation/e/vcard4android/Contact.kt b/src/main/java/foundation/e/vcard4android/Contact.kt
index 5147dbed6ee436619a1091e23237654f7f221db5..59052fce32a9ae5f6fbe34474574ed280a03ecd8 100644
--- a/src/main/java/foundation/e/vcard4android/Contact.kt
+++ b/src/main/java/foundation/e/vcard4android/Contact.kt
@@ -11,6 +11,7 @@ package foundation.e.vcard4android
import ezvcard.Ezvcard
import ezvcard.VCard
import ezvcard.VCardVersion
+import ezvcard.parameter.AddressType
import ezvcard.parameter.EmailType
import ezvcard.parameter.ImageType
import ezvcard.parameter.TelephoneType
@@ -90,8 +91,16 @@ class Contact {
val PHONE_TYPE_RADIO = TelephoneType.get("x-radio")!!
val PHONE_TYPE_ASSISTANT = TelephoneType.get("X-assistant")!!
val PHONE_TYPE_MMS = TelephoneType.get("x-mms")!!
+ /** Sometimes used to denote an "other" phone numbers. Only for compatibility – don't use it yourself! */
+ val PHONE_TYPE_OTHER = TelephoneType.get("other")!!
+ /** Custom email type to denote "mobile" email addresses. */
val EMAIL_TYPE_MOBILE = EmailType.get("x-mobile")!!
+ /** Sometimes used to denote an "other" email address. Only for compatibility – don't use it yourself! */
+ val EMAIL_TYPE_OTHER = EmailType.get("other")!!
+
+ /** Sometimes used to denote an "other" postal address. Only for compatibility – don't use it yourself! */
+ val ADDRESS_TYPE_OTHER = AddressType.get("other")!!
const val NICKNAME_TYPE_MAIDEN_NAME = "x-maiden-name"
const val NICKNAME_TYPE_SHORT_NAME = "x-short-name"
@@ -108,12 +117,13 @@ class Contact {
/**
- * Parses an InputStream that contains a VCard.
+ * Parses an InputStream that contains a vCard.
*
- * @param reader reader for the input stream containing the VCard (pay attention to the charset)
+ * @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 {
val vcards = Ezvcard.parse(reader).all()
@@ -222,7 +232,7 @@ class Contact {
extToRemove.distinct().forEach { vCard.removeExtendedProperty(it.propertyName) }
if (c.uid == null) {
- Constants.log.warning("Received VCard without UID, generating new one")
+ Constants.log.warning("Received vCard without UID, generating new one")
c.uid = UUID.randomUUID().toString()
}
@@ -296,7 +306,7 @@ class Contact {
try {
unknownProperties?.let { vCard = Ezvcard.parse(unknownProperties).first() }
} catch (e: Exception) {
- Constants.log.fine("Couldn't parse original VCard with retained properties")
+ Constants.log.fine("Couldn't parse original vCard with retained properties")
}
// UID
@@ -309,7 +319,7 @@ class Contact {
if (vCardVersion == VCardVersion.V4_0) {
vCard.kind = Kind.group()
members.forEach { vCard.members += Member("urn:uuid:$it") }
- } else { // "VCard4 as VCard3" (Apple-style)
+ } else { // "vCard4 as vCard3" (Apple-style)
vCard.setExtendedProperty(PROPERTY_ADDRESSBOOKSERVER_KIND, Kind.GROUP)
members.forEach { vCard.addExtendedProperty(PROPERTY_ADDRESSBOOKSERVER_MEMBER, "urn:uuid:$it") }
}
@@ -346,7 +356,7 @@ class Contact {
vCard.structuredName = n
} else if (vCardVersion == VCardVersion.V3_0) {
- // (only) VCard 3 requires N [RFC 2426 3.1.2]
+ // (only) vCard 3 requires N [RFC 2426 3.1.2]
if (group && groupMethod == GroupMethod.GROUP_VCARDS) {
val n = StructuredName()
n.family = fn
@@ -373,7 +383,7 @@ class Contact {
fun addLabel(labeledProperty: LabeledProperty) {
labeledProperty.label?.let {
- val group = "davdroid${labelIterator.incrementAndGet()}"
+ val group = "group${labelIterator.incrementAndGet()}"
labeledProperty.property.group = group
val label = vCard.addExtendedProperty(LabeledProperty.PROPERTY_AB_LABEL, it)
@@ -431,17 +441,17 @@ class Contact {
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
+ // 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
+ // 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, Integer.toString(DATE_PARAMETER_OMIT_YEAR_DEFAULT))
+ fakeProp.addParameter(DATE_PARAMETER_OMIT_YEAR, DATE_PARAMETER_OMIT_YEAR_DEFAULT.toString())
fakeProp
}
}
@@ -460,20 +470,20 @@ class Contact {
// REV
vCard.revision = Revision.now()
- // validate VCard and log results
+ // validate vCard and log results
val validation = vCard.validate(vCardVersion)
if (!validation.isEmpty) {
val msgs = LinkedList()
for ((key, warnings) in validation)
msgs += " * " + key.javaClass.simpleName + " - " + warnings.joinToString(" | ")
- Constants.log.log(Level.INFO, "Generating possibly invalid VCard", msgs.joinToString(","))
+ Constants.log.log(Level.WARNING, "vCard validation warnings", msgs.joinToString(","))
}
// generate VCARD
Ezvcard .write(vCard)
.version(vCardVersion)
- .versionStrict(false) // allow VCard4 properties in VCard3s
- .caretEncoding(true) // enable RFC 6868 support
+ .versionStrict(false) // allow vCard4 properties in vCard3s
+ .caretEncoding(true) // enable RFC 6868 support
.prodId(productID == null)
.go(os)
}
diff --git a/src/main/res/values/about_strings.xml b/src/main/res/values/about_strings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..44462aded4ff628c8e66ab91db19ae87b87ab08f
--- /dev/null
+++ b/src/main/res/values/about_strings.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ Ricki Hirner
+ https://www.bitfire.at
+ vcard4android
+ Allows usage of vCard resources with the Android contacts provider
+ https://gitlab.com/bitfireAT/vcard4android
+ gpl_3_0
+ true
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
deleted file mode 100644
index 1072878777a4a2b89fb5d44010f5711f2abc250e..0000000000000000000000000000000000000000
--- a/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- VCard4Android
-
diff --git a/src/test/java/foundation/e/vcard4android/ContactTest.kt b/src/test/java/foundation/e/vcard4android/ContactTest.kt
index 9fd81f075936d5d52753d9c01e78ba6eea2e27e2..3caef9cb50a8f635f0d71aa8c3113436867db704 100644
--- a/src/test/java/foundation/e/vcard4android/ContactTest.kt
+++ b/src/test/java/foundation/e/vcard4android/ContactTest.kt
@@ -26,7 +26,7 @@ import java.util.*
class ContactTest {
private fun parseContact(fname: String, charset: Charset = Charsets.UTF_8) =
- javaClass.classLoader.getResourceAsStream(fname).use { stream ->
+ javaClass.classLoader!!.getResourceAsStream(fname).use { stream ->
Contact.fromReader(InputStreamReader(stream, charset), null).first()
}
@@ -175,8 +175,8 @@ class ContactTest {
c.uid = UUID.randomUUID().toString()
c.phoneNumbers += LabeledProperty(Telephone("12345"), "My Phone")
val vCard = toString(c, GroupMethod.GROUP_VCARDS, VCardVersion.V3_0)
- assertTrue(vCard.contains("\ndavdroid1.TEL:12345\r\n"))
- assertTrue(vCard.contains("\ndavdroid1.X-ABLabel:My Phone\r\n"))
+ assertTrue(vCard.contains("\ngroup1.TEL:12345\r\n"))
+ assertTrue(vCard.contains("\ngroup1.X-ABLabel:My Phone\r\n"))
c = regenerate(c, VCardVersion.V4_0)
assertEquals("12345", c.phoneNumbers.first.property.text)
@@ -336,7 +336,7 @@ class ContactTest {
assertEquals("muuuum", rel.text)
// PHOTO
- javaClass.classLoader.getResourceAsStream("lol.jpg").use { photo ->
+ javaClass.classLoader!!.getResourceAsStream("lol.jpg").use { photo ->
assertArrayEquals(IOUtils.toByteArray(photo), c.photo)
}
}