From c0720e3e7311b6299f979ae732ecbe7cd43df664 Mon Sep 17 00:00:00 2001 From: tibbi Date: Thu, 23 Aug 2018 23:23:45 +0200 Subject: [PATCH] rewriting contact exporting/importing, rely on vcard at parsing --- app/build.gradle | 1 + .../contacts/activities/ContactActivity.kt | 31 -- .../activities/EditContactActivity.kt | 4 +- .../contacts/activities/MainActivity.kt | 2 +- .../activities/ViewContactActivity.kt | 2 +- .../contacts/extensions/String.kt | 34 ++ .../contacts/helpers/Constants.kt | 21 -- .../contacts/helpers/VcfExporter.kt | 203 +++++----- .../contacts/helpers/VcfImporter.kt | 354 ++++-------------- build.gradle | 2 +- 10 files changed, 215 insertions(+), 439 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt diff --git a/app/build.gradle b/app/build.gradle index 75606435..dd3e3290 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation 'joda-time:joda-time:2.9.9' implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.android.support.constraint:constraint-layout:1.1.2' + compile 'com.googlecode.ez-vcard:ez-vcard:0.10.4' debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ContactActivity.kt index 732ff08c..77e4e764 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ContactActivity.kt @@ -4,7 +4,6 @@ import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.provider.ContactsContract import android.widget.ImageView -import android.widget.TextView import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -17,7 +16,6 @@ import com.simplemobiletools.commons.dialogs.ConfirmationDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.getColoredBitmap import com.simplemobiletools.commons.extensions.getContrastColor -import com.simplemobiletools.commons.helpers.getDateFormats import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.extensions.config @@ -26,10 +24,6 @@ import com.simplemobiletools.contacts.extensions.sendSMSIntent import com.simplemobiletools.contacts.extensions.shareContacts import com.simplemobiletools.contacts.helpers.ContactsHelper import com.simplemobiletools.contacts.models.Contact -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormat -import java.text.DateFormat -import java.text.SimpleDateFormat import java.util.* abstract class ContactActivity : SimpleActivity() { @@ -68,31 +62,6 @@ abstract class ContactActivity : SimpleActivity() { }).into(photoView) } - fun getDateTime(dateString: String, viewToUpdate: TextView? = null): DateTime { - val dateFormats = getDateFormats() - var date = DateTime() - for (format in dateFormats) { - try { - date = DateTime.parse(dateString, DateTimeFormat.forPattern(format)) - - val formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()) - var localPattern = (formatter as SimpleDateFormat).toLocalizedPattern() - - val hasYear = format.contains("y") - if (!hasYear) { - localPattern = localPattern.replace("y", "").trim() - date = date.withYear(DateTime().year) - } - - val formattedString = date.toString(localPattern) - viewToUpdate?.text = formattedString - break - } catch (ignored: Exception) { - } - } - return date - } - fun deleteContact() { ConfirmationDialog(this) { if (contact != null) { diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/EditContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/EditContactActivity.kt index ad80cb37..5acf55af 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/EditContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/EditContactActivity.kt @@ -427,7 +427,7 @@ class EditContactActivity : ContactActivity() { (eventHolder as ViewGroup).apply { val contactEvent = contact_event.apply { - getDateTime(event.value, this) + event.value.getDateTimeFromDateString(this) tag = event.value alpha = 1f } @@ -595,7 +595,7 @@ class EditContactActivity : ContactActivity() { } } - val date = getDateTime(eventField.tag?.toString() ?: "") + val date = (eventField.tag?.toString() ?: "").getDateTimeFromDateString() DatePickerDialog(this, getDialogTheme(), setDateListener, date.year, date.monthOfYear - 1, date.dayOfMonth).show() } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt index d080242b..19c92a75 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/MainActivity.kt @@ -73,8 +73,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener { } else { checkContactPermissions() } - } + storeStateVariables() checkWhatsNewDialog() } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt index 399fa024..3b41f777 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/activities/ViewContactActivity.kt @@ -293,7 +293,7 @@ class ViewContactActivity : ContactActivity() { layoutInflater.inflate(R.layout.item_event, contact_events_holder, false).apply { contact_events_holder.addView(this) contact_event.alpha = 1f - getDateTime(it.value, contact_event) + it.value.getDateTimeFromDateString(contact_event) contact_event_type.setText(getEventTextId(it.type)) contact_event_remove.beGone() } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt new file mode 100644 index 00000000..6134b1ad --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/extensions/String.kt @@ -0,0 +1,34 @@ +package com.simplemobiletools.contacts.extensions + +import android.widget.TextView +import com.simplemobiletools.commons.helpers.getDateFormats +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* + +fun String.getDateTimeFromDateString(viewToUpdate: TextView? = null): DateTime { + val dateFormats = getDateFormats() + var date = DateTime() + for (format in dateFormats) { + try { + date = DateTime.parse(this, DateTimeFormat.forPattern(format)) + + val formatter = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()) + var localPattern = (formatter as SimpleDateFormat).toLocalizedPattern() + + val hasYear = format.contains("y") + if (!hasYear) { + localPattern = localPattern.replace("y", "").trim() + date = date.withYear(DateTime().year) + } + + val formattedString = date.toString(localPattern) + viewToUpdate?.text = formattedString + break + } catch (ignored: Exception) { + } + } + return date +} diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt index a1c7e3a5..0f56d22b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/Constants.kt @@ -52,26 +52,6 @@ const val PHOTO_REMOVED = 2 const val PHOTO_CHANGED = 3 const val PHOTO_UNCHANGED = 4 -// export/import -const val BEGIN_VCARD = "BEGIN:VCARD" -const val END_VCARD = "END:VCARD" -const val N = "N" -const val NICKNAME = "NICKNAME" -const val TEL = "TEL" -const val BDAY = "BDAY:" -const val ANNIVERSARY = "ANNIVERSARY:" -const val PHOTO = "PHOTO" -const val EMAIL = "EMAIL" -const val ADR = "ADR" -const val NOTE = "NOTE" -const val ORG = "ORG" -const val TITLE = "TITLE" -const val URL = "URL" -const val ENCODING = "ENCODING" -const val BASE64 = "BASE64" -const val JPEG = "JPEG" -const val VERSION_2_1 = "VERSION:2.1" - // phone number/email types const val CELL = "CELL" const val WORK = "WORK" @@ -83,7 +63,6 @@ const val WORK_FAX = "WORK;FAX" const val HOME_FAX = "HOME;FAX" const val PAGER = "PAGER" const val MOBILE = "MOBILE" -const val VOICE = "VOICE" const val ON_CLICK_CALL_CONTACT = 1 const val ON_CLICK_VIEW_CONTACT = 2 diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt index 5e6adf06..4832c2ac 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfExporter.kt @@ -1,22 +1,30 @@ package com.simplemobiletools.contacts.helpers -import android.graphics.Bitmap import android.net.Uri import android.provider.ContactsContract.CommonDataKinds import android.provider.MediaStore -import android.util.Base64 import com.simplemobiletools.commons.activities.BaseSimpleActivity -import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.extensions.getFileOutputStream +import com.simplemobiletools.commons.extensions.showErrorToast +import com.simplemobiletools.commons.extensions.toFileDirItem +import com.simplemobiletools.commons.extensions.toast import com.simplemobiletools.contacts.R +import com.simplemobiletools.contacts.extensions.getByteArray +import com.simplemobiletools.contacts.extensions.getDateTimeFromDateString import com.simplemobiletools.contacts.helpers.VcfExporter.ExportResult.EXPORT_FAIL import com.simplemobiletools.contacts.models.Contact -import java.io.BufferedWriter -import java.io.ByteArrayOutputStream +import ezvcard.Ezvcard +import ezvcard.VCard +import ezvcard.parameter.AddressType +import ezvcard.parameter.EmailType +import ezvcard.parameter.ImageType +import ezvcard.parameter.TelephoneType +import ezvcard.property.* +import ezvcard.util.PartialDate import java.io.File +import java.util.* class VcfExporter { - private val ENCODED_PHOTO_LINE_LENGTH = 74 - enum class ExportResult { EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL } @@ -36,65 +44,93 @@ class VcfExporter { activity.toast(R.string.exporting) } - it.bufferedWriter().use { out -> - for (contact in contacts) { - out.writeLn(BEGIN_VCARD) - out.writeLn(VERSION_2_1) - out.writeLn("$N${getNames(contact)}") + val cards = ArrayList() + for (contact in contacts) { + val card = VCard() + StructuredName().apply { + prefixes.add(contact.prefix) + given = contact.firstName + additionalNames.add(contact.middleName) + family = contact.surname + suffixes.add(contact.suffix) + card.structuredName = this + } - if (contact.nickname.isNotEmpty()) { - out.writeLn("$NICKNAME:${contact.nickname}") - } + if (contact.nickname.isNotEmpty()) { + card.setNickname(contact.nickname) + } - contact.phoneNumbers.forEach { - out.writeLn("$TEL;${getPhoneNumberLabel(it.type)}:${it.value}") - } + contact.phoneNumbers.forEach { + val phoneNumber = Telephone(it.value) + phoneNumber.types.add(TelephoneType.find(getPhoneNumberLabel(it.type))) + card.addTelephoneNumber(phoneNumber) + } - contact.emails.forEach { - val type = getEmailTypeLabel(it.type) - val delimiterType = if (type.isEmpty()) "" else ";$type" - out.writeLn("$EMAIL$delimiterType:${it.value}") - } + contact.emails.forEach { + val email = Email(it.value) + email.types.add(EmailType.find(getEmailTypeLabel(it.type))) + card.addEmail(email) + } - contact.addresses.forEach { - val type = getAddressTypeLabel(it.type) - val delimiterType = if (type.isEmpty()) "" else ";$type" - out.writeLn("$ADR$delimiterType:;;${it.value.replace("\n", "\\n")};;;;") - } - - contact.events.forEach { - if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { - out.writeLn("$BDAY${it.value}") + contact.events.forEach { + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY || it.type == CommonDataKinds.Event.TYPE_ANNIVERSARY) { + val dateTime = it.value.getDateTimeFromDateString() + if (it.value.startsWith("--")) { + val partialDate = PartialDate.builder().year(null).month(dateTime.monthOfYear - 1).date(dateTime.dayOfMonth).build() + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { + card.birthdays.add(Birthday(partialDate)) + } else { + card.anniversaries.add(Anniversary(partialDate)) + } + } else { + Calendar.getInstance().apply { + clear() + set(Calendar.YEAR, dateTime.year) + set(Calendar.MONTH, dateTime.monthOfYear - 1) + set(Calendar.DAY_OF_MONTH, dateTime.dayOfMonth) + if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { + card.birthdays.add(Birthday(time)) + } else { + card.anniversaries.add(Anniversary(time)) + } + } } } - - if (contact.notes.isNotEmpty()) { - out.writeLn("$NOTE:${contact.notes.replace("\n", "\\n")}") - } - - if (!contact.organization.isEmpty()) { - out.writeLn("$ORG:${contact.organization.company.replace("\n", "\\n")}") - out.writeLn("$TITLE:${contact.organization.jobPosition.replace("\n", "\\n")}") - } - - contact.websites.forEach { - out.writeLn("$URL:$it") - } - - if (contact.thumbnailUri.isNotEmpty()) { - val bitmap = MediaStore.Images.Media.getBitmap(activity.contentResolver, Uri.parse(contact.thumbnailUri)) - addBitmap(bitmap, out) - } - - if (contact.photo != null) { - addBitmap(contact.photo!!, out) - } - - out.writeLn(END_VCARD) - contactsExported++ } + + contact.addresses.forEach { + val address = Address() + address.streetAddress = it.value + address.types.add(AddressType.find(getAddressTypeLabel(it.type))) + card.addAddress(address) + } + + if (contact.notes.isNotEmpty()) { + card.addNote(contact.notes) + } + + if (!contact.organization.isEmpty()) { + val organization = Organization() + organization.values.add(contact.organization.company) + card.organization = organization + card.titles.add(Title(contact.organization.jobPosition)) + } + + contact.websites.forEach { + card.addUrl(it) + } + + if (contact.thumbnailUri.isNotEmpty()) { + val photoByteArray = MediaStore.Images.Media.getBitmap(activity.contentResolver, Uri.parse(contact.thumbnailUri)).getByteArray() + val photo = Photo(photoByteArray, ImageType.JPEG) + card.addPhoto(photo) + } + + cards.add(card) + contactsExported++ } + Ezvcard.write(cards).go(file) } catch (e: Exception) { activity.showErrorToast(e) } @@ -107,71 +143,24 @@ class VcfExporter { } } - private fun addBitmap(bitmap: Bitmap, out: BufferedWriter) { - val firstLine = "$PHOTO;$ENCODING=$BASE64;$JPEG:" - val byteArrayOutputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.JPEG, 85, byteArrayOutputStream) - bitmap.recycle() - val byteArray = byteArrayOutputStream.toByteArray() - val encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP) - - val encodedFirstLineSection = encoded.substring(0, ENCODED_PHOTO_LINE_LENGTH - firstLine.length) - out.writeLn(firstLine + encodedFirstLineSection) - var curStartIndex = encodedFirstLineSection.length - do { - val part = encoded.substring(curStartIndex, Math.min(curStartIndex + ENCODED_PHOTO_LINE_LENGTH - 1, encoded.length)) - out.writeLn(" $part") - curStartIndex += ENCODED_PHOTO_LINE_LENGTH - 1 - } while (curStartIndex < encoded.length) - - out.writeLn("") - } - - private fun getNames(contact: Contact): String { - var result = "" - var firstName = contact.firstName - var surName = contact.surname - var middleName = contact.middleName - var prefix = contact.prefix - var suffix = contact.suffix - - if (QuotedPrintable.urlEncode(firstName) != firstName - || QuotedPrintable.urlEncode(surName) != surName - || QuotedPrintable.urlEncode(middleName) != middleName - || QuotedPrintable.urlEncode(prefix) != prefix - || QuotedPrintable.urlEncode(suffix) != suffix) { - firstName = QuotedPrintable.encode(firstName) - surName = QuotedPrintable.encode(surName) - middleName = QuotedPrintable.encode(middleName) - prefix = QuotedPrintable.encode(prefix) - suffix = QuotedPrintable.encode(suffix) - result += ";CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE" - } - - return "$result:$surName;$firstName;$middleName;$prefix;$suffix" - } - private fun getPhoneNumberLabel(type: Int) = when (type) { CommonDataKinds.Phone.TYPE_MOBILE -> CELL - CommonDataKinds.Phone.TYPE_HOME -> HOME CommonDataKinds.Phone.TYPE_WORK -> WORK CommonDataKinds.Phone.TYPE_MAIN -> PREF CommonDataKinds.Phone.TYPE_FAX_WORK -> WORK_FAX CommonDataKinds.Phone.TYPE_FAX_HOME -> HOME_FAX CommonDataKinds.Phone.TYPE_PAGER -> PAGER - else -> VOICE + else -> HOME } private fun getEmailTypeLabel(type: Int) = when (type) { - CommonDataKinds.Email.TYPE_HOME -> HOME CommonDataKinds.Email.TYPE_WORK -> WORK CommonDataKinds.Email.TYPE_MOBILE -> MOBILE - else -> "" + else -> HOME } private fun getAddressTypeLabel(type: Int) = when (type) { - CommonDataKinds.StructuredPostal.TYPE_HOME -> HOME CommonDataKinds.StructuredPostal.TYPE_WORK -> WORK - else -> "" + else -> HOME } } diff --git a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt index c50168cc..d247b0ba 100644 --- a/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/contacts/helpers/VcfImporter.kt @@ -3,8 +3,6 @@ package com.simplemobiletools.contacts.helpers import android.graphics.Bitmap import android.graphics.BitmapFactory import android.provider.ContactsContract.CommonDataKinds -import android.text.TextUtils -import android.util.Base64 import android.widget.Toast import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.contacts.activities.SimpleActivity @@ -12,45 +10,19 @@ import com.simplemobiletools.contacts.extensions.getCachePhoto import com.simplemobiletools.contacts.extensions.getCachePhotoUri import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.* import com.simplemobiletools.contacts.models.* +import ezvcard.Ezvcard +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormat import java.io.File import java.io.FileOutputStream +import java.util.* class VcfImporter(val activity: SimpleActivity) { enum class ImportResult { IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL } - private var curPrefix = "" - private var curFirstName = "" - private var curMiddleName = "" - private var curSurname = "" - private var curSuffix = "" - private var curNickname = "" - private var curPhotoUri = "" - private var curNotes = "" - private var curCompany = "" - private var curJobPosition = "" - private var curPhoneNumbers = ArrayList() - private var curEmails = ArrayList() - private var curEvents = ArrayList() - private var curAddresses = ArrayList
() - private var curGroups = ArrayList() - private var curWebsites = ArrayList() - - private var isGettingPhoto = false - private var currentPhotoString = StringBuilder() - private var currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG - - private var isGettingName = false - private var currentNameIsANSI = false - private var currentNameString = StringBuilder() - - private var isGettingNotes = false - private var currentNotesSB = StringBuilder() - - private var isGettingCompany = false - private var currentCompanyIsANSI = false - private var currentCompany = StringBuilder() + private val PATTERN = "EEE MMM dd HH:mm:ss 'GMT'ZZ YYYY" private var contactsImported = 0 private var contactsFailed = 0 @@ -63,51 +35,70 @@ class VcfImporter(val activity: SimpleActivity) { activity.assets.open(path) } - inputStream.bufferedReader().use { - while (true) { - val originalLine = it.readLine() ?: break - val line = originalLine.trim() - if (line.isEmpty()) { - if (isGettingPhoto) { - savePhoto() - isGettingPhoto = false - } - continue - } else if (line.startsWith('\t') && isGettingName) { - currentNameString.append(line.trimStart('\t')) - isGettingName = false - parseNames() - } else if (isGettingNotes) { - if (originalLine.startsWith(' ')) { - currentNotesSB.append(line.substring(1)) - } else { - curNotes = currentNotesSB.toString().replace("\\n", "\n").replace("\\,", ",") - isGettingNotes = false - } - } else if (isGettingCompany && currentCompanyIsANSI && line.startsWith("=")) { - currentCompany.append(line) - curCompany = QuotedPrintable.decode(currentCompany.toString().replace("==", "=")) - continue - } + val ezContacts = Ezvcard.parse(inputStream).all() + for (ezContact in ezContacts) { + val structuredName = ezContact.structuredName + val prefix = structuredName?.prefixes?.firstOrNull() ?: "" + val firstName = structuredName?.given ?: "" + val middleName = structuredName?.additionalNames?.firstOrNull() ?: "" + val surname = structuredName?.family ?: "" + val suffix = structuredName?.suffixes?.firstOrNull() ?: "" + val nickname = ezContact.nickname?.values?.firstOrNull() ?: "" + val photoUri = "" - when { - line.toUpperCase() == BEGIN_VCARD -> resetValues() - line.toUpperCase().startsWith(NOTE) -> addNotes(line.substring(NOTE.length)) - line.toUpperCase().startsWith(NICKNAME) -> addNickname(line.substring(NICKNAME.length)) - line.toUpperCase().startsWith(N) -> addNames(line.substring(N.length)) - line.toUpperCase().startsWith(TEL) -> addPhoneNumber(line.substring(TEL.length)) - line.toUpperCase().startsWith(EMAIL) -> addEmail(line.substring(EMAIL.length)) - line.toUpperCase().startsWith(ADR) -> addAddress(line.substring(ADR.length)) - line.toUpperCase().startsWith(BDAY) -> addBirthday(line.substring(BDAY.length)) - line.toUpperCase().startsWith(ANNIVERSARY) -> addAnniversary(line.substring(ANNIVERSARY.length)) - line.toUpperCase().startsWith(PHOTO) -> addPhoto(line.substring(PHOTO.length)) - line.toUpperCase().startsWith(ORG) -> addCompany(line.substring(ORG.length)) - line.toUpperCase().startsWith(TITLE) -> addJobPosition(line.substring(TITLE.length)) - line.toUpperCase().startsWith(URL) -> addWebsite(line.substring(URL.length)) - line.toUpperCase() == END_VCARD -> saveContact(targetContactSource) - isGettingPhoto -> currentPhotoString.append(line.trim()) + val phoneNumbers = ArrayList() + ezContact.telephoneNumbers.forEach { + val type = getPhoneNumberTypeId(it.types.firstOrNull()?.value ?: MOBILE) + val number = it.text + phoneNumbers.add(PhoneNumber(number, type)) + } + + val emails = ArrayList() + ezContact.emails.forEach { + val type = getEmailTypeId(it.types.firstOrNull()?.value ?: HOME) + val email = it.value + emails.add(Email(email, type)) + } + + val addresses = ArrayList
() + ezContact.addresses.forEach { + val type = getAddressTypeId(it.types.firstOrNull()?.value ?: HOME) + val address = it.streetAddress + if (address?.isNotEmpty() == true) { + addresses.add(Address(address, type)) } } + + val events = ArrayList() + ezContact.birthdays.forEach { + val event = Event(formatDateToDayCode(it.date), CommonDataKinds.Event.TYPE_BIRTHDAY) + events.add(event) + } + + ezContact.anniversaries.forEach { + val event = Event(formatDateToDayCode(it.date), CommonDataKinds.Event.TYPE_ANNIVERSARY) + events.add(event) + } + + val starred = 0 + val contactId = 0 + val notes = ezContact.notes.firstOrNull()?.value ?: "" + val groups = ArrayList() + val company = ezContact.organization?.values?.firstOrNull() ?: "" + val jobPosition = ezContact.titles?.firstOrNull()?.value ?: "" + val organization = Organization(company, jobPosition) + val websites = ezContact.urls.map { it.value } as ArrayList + + val photoData = ezContact.photos.firstOrNull()?.data + val photo = null + val thumbnailUri = savePhoto(photoData) + + val contact = Contact(0, prefix, firstName, middleName, surname, suffix, nickname, photoUri, phoneNumbers, emails, addresses, events, + targetContactSource, starred, contactId, thumbnailUri, photo, notes, groups, organization, websites) + + if (ContactsHelper(activity).insertContact(contact)) { + contactsImported++ + } } } catch (e: Exception) { activity.showErrorToast(e, Toast.LENGTH_LONG) @@ -121,238 +112,51 @@ class VcfImporter(val activity: SimpleActivity) { } } - private fun addNames(names: String) { - val parts = names.split(":") - currentNameIsANSI = parts.first().toUpperCase().contains("QUOTED-PRINTABLE") - currentNameString.append(parts[1].trimEnd('=')) - if (!isGettingName && currentNameIsANSI && names.endsWith('=')) { - isGettingName = true - } else { - if (names.contains(";")) { - parseNames() - } else if (names.startsWith(":")) { - curFirstName = names.substring(1) - } - } + private fun formatDateToDayCode(date: Date): String { + val dateTime = DateTime.parse(date.toString(), DateTimeFormat.forPattern(PATTERN)) + return dateTime.toString("yyyy-MM-dd") } - private fun parseNames() { - val nameParts = currentNameString.split(";") - curSurname = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[0]) else nameParts[0] - curFirstName = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[1]) else nameParts[1] - - if (nameParts.size > 2) { - curMiddleName = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[2]) else nameParts[2] - curPrefix = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[3]) else nameParts[3] - curSuffix = if (currentNameIsANSI) QuotedPrintable.decode(nameParts[4]) else nameParts[4] - } - } - - private fun addNickname(nickname: String) { - curNickname = if (nickname.startsWith(";CHARSET", true)) { - nickname.substringAfter(":") - } else { - nickname.substring(1) - } - } - - private fun addPhoneNumber(phoneNumber: String) { - val phoneParts = phoneNumber.trimStart(';').split(":") - var rawType = phoneParts[0] - var subType = "" - if (rawType.contains('=')) { - val types = rawType.split('=') - if (types.any { it.contains(';') }) { - subType = types[1].split(';')[0] - } - rawType = types.last() - } - - val type = getPhoneNumberTypeId(rawType.toUpperCase(), subType) - val value = phoneParts[1] - curPhoneNumbers.add(PhoneNumber(value, type)) - } - - private fun getPhoneNumberTypeId(type: String, subType: String) = when (type) { + private fun getPhoneNumberTypeId(type: String) = when (type.toUpperCase()) { CELL -> CommonDataKinds.Phone.TYPE_MOBILE HOME -> CommonDataKinds.Phone.TYPE_HOME WORK -> CommonDataKinds.Phone.TYPE_WORK PREF, MAIN -> CommonDataKinds.Phone.TYPE_MAIN WORK_FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK HOME_FAX -> CommonDataKinds.Phone.TYPE_FAX_HOME - FAX -> if (subType == WORK) CommonDataKinds.Phone.TYPE_FAX_WORK else CommonDataKinds.Phone.TYPE_FAX_HOME + FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK PAGER -> CommonDataKinds.Phone.TYPE_PAGER else -> CommonDataKinds.Phone.TYPE_OTHER } - private fun addEmail(email: String) { - val emailParts = email.trimStart(';').split(":") - var rawType = emailParts[0] - if (rawType.contains('=')) { - rawType = rawType.split('=').last() - } - val type = getEmailTypeId(rawType.toUpperCase()) - val value = emailParts[1] - curEmails.add(Email(value, type)) - } - - private fun getEmailTypeId(type: String) = when (type) { + private fun getEmailTypeId(type: String) = when (type.toUpperCase()) { HOME -> CommonDataKinds.Email.TYPE_HOME WORK -> CommonDataKinds.Email.TYPE_WORK MOBILE -> CommonDataKinds.Email.TYPE_MOBILE else -> CommonDataKinds.Email.TYPE_OTHER } - private fun addAddress(address: String) { - val addressParts = address.trimStart(';').split(":") - var rawType = addressParts[0] - if (rawType.contains('=')) { - rawType = rawType.split('=').last() - } - - val type = getAddressTypeId(rawType.toUpperCase()) - val addresses = addressParts[1].split(";") - if (addresses.size == 7) { - var parsedAddress = if (address.contains(";CHARSET=UTF-8:")) { - TextUtils.join(", ", addresses.filter { it.trim().isNotEmpty() }) - } else { - addresses[2].replace("\\n", "\n") - } - - if (address.contains("QUOTED-PRINTABLE")) { - parsedAddress = QuotedPrintable.decode(parsedAddress) - } - - curAddresses.add(Address(parsedAddress, type)) - } - } - - private fun getAddressTypeId(type: String) = when (type) { + private fun getAddressTypeId(type: String) = when (type.toUpperCase()) { HOME -> CommonDataKinds.Email.TYPE_HOME WORK -> CommonDataKinds.Email.TYPE_WORK else -> CommonDataKinds.Email.TYPE_OTHER } - private fun addBirthday(birthday: String) { - curEvents.add(Event(birthday, CommonDataKinds.Event.TYPE_BIRTHDAY)) - } - - private fun addAnniversary(anniversary: String) { - curEvents.add(Event(anniversary, CommonDataKinds.Event.TYPE_ANNIVERSARY)) - } - - private fun addPhoto(photo: String) { - val photoParts = photo.trimStart(';').split(';') - if (photoParts.size == 2) { - val typeParts = photoParts[1].split(':') - currentPhotoCompressionFormat = getPhotoCompressionFormat(typeParts[0]) - val encoding = photoParts[0].split('=').last() - if (encoding == BASE64) { - isGettingPhoto = true - currentPhotoString.append(typeParts[1].trim()) - } + private fun savePhoto(byteArray: ByteArray?): String { + if (byteArray == null) { + return "" } - } - private fun getPhotoCompressionFormat(type: String) = when (type.toLowerCase()) { - "png" -> Bitmap.CompressFormat.PNG - "webp" -> Bitmap.CompressFormat.WEBP - else -> Bitmap.CompressFormat.JPEG - } - - private fun savePhoto() { val file = activity.getCachePhoto() - val imageAsBytes = Base64.decode(currentPhotoString.toString().toByteArray(), Base64.DEFAULT) - val bitmap = BitmapFactory.decodeByteArray(imageAsBytes, 0, imageAsBytes.size) + val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) var fileOutputStream: FileOutputStream? = null try { fileOutputStream = FileOutputStream(file) - bitmap.compress(currentPhotoCompressionFormat, 100, fileOutputStream) + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream) } finally { fileOutputStream?.close() } - curPhotoUri = activity.getCachePhotoUri(file).toString() - } - - private fun addNotes(notes: String) { - if (notes.startsWith(";CHARSET", true)) { - currentNotesSB.append(notes.substringAfter(":")) - } else { - currentNotesSB.append(notes.substring(1)) - } - isGettingNotes = true - } - - private fun addCompany(company: String) { - curCompany = if (company.startsWith(";")) { - company.substringAfter(":").trim(';') - } else { - company.trimStart(':') - } - - currentCompanyIsANSI = company.toUpperCase().contains("QUOTED-PRINTABLE") - currentCompany.append(curCompany) - isGettingCompany = true - } - - private fun addJobPosition(jobPosition: String) { - curJobPosition = if (jobPosition.startsWith(";")) { - jobPosition.substringAfter(":") - } else { - jobPosition.trimStart(':') - } - } - - private fun addWebsite(website: String) { - if (website.startsWith(";")) { - curWebsites.add(website.substringAfter(":")) - } else { - curWebsites.add(website.trimStart(':')) - } - } - - private fun saveContact(source: String) { - val organization = Organization(curCompany, curJobPosition) - val contact = Contact(0, curPrefix, curFirstName, curMiddleName, curSurname, curSuffix, curNickname, curPhotoUri, curPhoneNumbers, - curEmails, curAddresses, curEvents, source, 0, 0, "", null, curNotes, curGroups, organization, curWebsites) - - if (ContactsHelper(activity).insertContact(contact)) { - contactsImported++ - } - } - - private fun resetValues() { - curPrefix = "" - curFirstName = "" - curMiddleName = "" - curSurname = "" - curSuffix = "" - curNickname = "" - curPhotoUri = "" - curNotes = "" - curCompany = "" - curJobPosition = "" - curPhoneNumbers = ArrayList() - curEmails = ArrayList() - curEvents = ArrayList() - curAddresses = ArrayList() - curGroups = ArrayList() - curWebsites = ArrayList() - - isGettingPhoto = false - currentPhotoString = StringBuilder() - currentPhotoCompressionFormat = Bitmap.CompressFormat.JPEG - - isGettingName = false - currentNameIsANSI = false - currentNameString = StringBuilder() - - isGettingNotes = false - currentNotesSB = StringBuilder() - - isGettingCompany = false - currentCompanyIsANSI = false - currentCompany = StringBuilder() + return activity.getCachePhotoUri(file).toString() } } diff --git a/build.gradle b/build.gradle index b0005124..1b8fb3c3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.2.60' + ext.kotlin_version = '1.2.61' repositories { google()