rewriting contact exporting/importing, rely on vcard at parsing

This commit is contained in:
tibbi 2018-08-23 23:23:45 +02:00
parent 389d6e9266
commit c0720e3e73
10 changed files with 215 additions and 439 deletions

View file

@ -49,6 +49,7 @@ dependencies {
implementation 'joda-time:joda-time:2.9.9' implementation 'joda-time:joda-time:2.9.9'
implementation 'com.facebook.stetho:stetho:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.2' 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" debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion"

View file

@ -4,7 +4,6 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.provider.ContactsContract import android.provider.ContactsContract
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy 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.dialogs.RadioGroupDialog
import com.simplemobiletools.commons.extensions.getColoredBitmap import com.simplemobiletools.commons.extensions.getColoredBitmap
import com.simplemobiletools.commons.extensions.getContrastColor import com.simplemobiletools.commons.extensions.getContrastColor
import com.simplemobiletools.commons.helpers.getDateFormats
import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.commons.models.RadioItem
import com.simplemobiletools.contacts.R import com.simplemobiletools.contacts.R
import com.simplemobiletools.contacts.extensions.config 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.extensions.shareContacts
import com.simplemobiletools.contacts.helpers.ContactsHelper import com.simplemobiletools.contacts.helpers.ContactsHelper
import com.simplemobiletools.contacts.models.Contact 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.* import java.util.*
abstract class ContactActivity : SimpleActivity() { abstract class ContactActivity : SimpleActivity() {
@ -68,31 +62,6 @@ abstract class ContactActivity : SimpleActivity() {
}).into(photoView) }).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() { fun deleteContact() {
ConfirmationDialog(this) { ConfirmationDialog(this) {
if (contact != null) { if (contact != null) {

View file

@ -427,7 +427,7 @@ class EditContactActivity : ContactActivity() {
(eventHolder as ViewGroup).apply { (eventHolder as ViewGroup).apply {
val contactEvent = contact_event.apply { val contactEvent = contact_event.apply {
getDateTime(event.value, this) event.value.getDateTimeFromDateString(this)
tag = event.value tag = event.value
alpha = 1f 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() DatePickerDialog(this, getDialogTheme(), setDateListener, date.year, date.monthOfYear - 1, date.dayOfMonth).show()
} }

View file

@ -73,8 +73,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
} else { } else {
checkContactPermissions() checkContactPermissions()
} }
} }
storeStateVariables() storeStateVariables()
checkWhatsNewDialog() checkWhatsNewDialog()
} }

View file

@ -293,7 +293,7 @@ class ViewContactActivity : ContactActivity() {
layoutInflater.inflate(R.layout.item_event, contact_events_holder, false).apply { layoutInflater.inflate(R.layout.item_event, contact_events_holder, false).apply {
contact_events_holder.addView(this) contact_events_holder.addView(this)
contact_event.alpha = 1f contact_event.alpha = 1f
getDateTime(it.value, contact_event) it.value.getDateTimeFromDateString(contact_event)
contact_event_type.setText(getEventTextId(it.type)) contact_event_type.setText(getEventTextId(it.type))
contact_event_remove.beGone() contact_event_remove.beGone()
} }

View file

@ -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
}

View file

@ -52,26 +52,6 @@ const val PHOTO_REMOVED = 2
const val PHOTO_CHANGED = 3 const val PHOTO_CHANGED = 3
const val PHOTO_UNCHANGED = 4 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 // phone number/email types
const val CELL = "CELL" const val CELL = "CELL"
const val WORK = "WORK" const val WORK = "WORK"
@ -83,7 +63,6 @@ const val WORK_FAX = "WORK;FAX"
const val HOME_FAX = "HOME;FAX" const val HOME_FAX = "HOME;FAX"
const val PAGER = "PAGER" const val PAGER = "PAGER"
const val MOBILE = "MOBILE" const val MOBILE = "MOBILE"
const val VOICE = "VOICE"
const val ON_CLICK_CALL_CONTACT = 1 const val ON_CLICK_CALL_CONTACT = 1
const val ON_CLICK_VIEW_CONTACT = 2 const val ON_CLICK_VIEW_CONTACT = 2

View file

@ -1,22 +1,30 @@
package com.simplemobiletools.contacts.helpers package com.simplemobiletools.contacts.helpers
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.provider.ContactsContract.CommonDataKinds import android.provider.ContactsContract.CommonDataKinds
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Base64
import com.simplemobiletools.commons.activities.BaseSimpleActivity 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.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.helpers.VcfExporter.ExportResult.EXPORT_FAIL
import com.simplemobiletools.contacts.models.Contact import com.simplemobiletools.contacts.models.Contact
import java.io.BufferedWriter import ezvcard.Ezvcard
import java.io.ByteArrayOutputStream 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.io.File
import java.util.*
class VcfExporter { class VcfExporter {
private val ENCODED_PHOTO_LINE_LENGTH = 74
enum class ExportResult { enum class ExportResult {
EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL EXPORT_FAIL, EXPORT_OK, EXPORT_PARTIAL
} }
@ -36,65 +44,93 @@ class VcfExporter {
activity.toast(R.string.exporting) activity.toast(R.string.exporting)
} }
it.bufferedWriter().use { out -> val cards = ArrayList<VCard>()
for (contact in contacts) { for (contact in contacts) {
out.writeLn(BEGIN_VCARD) val card = VCard()
out.writeLn(VERSION_2_1) StructuredName().apply {
out.writeLn("$N${getNames(contact)}") 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()) { if (contact.nickname.isNotEmpty()) {
out.writeLn("$NICKNAME:${contact.nickname}") card.setNickname(contact.nickname)
} }
contact.phoneNumbers.forEach { contact.phoneNumbers.forEach {
out.writeLn("$TEL;${getPhoneNumberLabel(it.type)}:${it.value}") val phoneNumber = Telephone(it.value)
} phoneNumber.types.add(TelephoneType.find(getPhoneNumberLabel(it.type)))
card.addTelephoneNumber(phoneNumber)
}
contact.emails.forEach { contact.emails.forEach {
val type = getEmailTypeLabel(it.type) val email = Email(it.value)
val delimiterType = if (type.isEmpty()) "" else ";$type" email.types.add(EmailType.find(getEmailTypeLabel(it.type)))
out.writeLn("$EMAIL$delimiterType:${it.value}") card.addEmail(email)
} }
contact.addresses.forEach { contact.events.forEach {
val type = getAddressTypeLabel(it.type) if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY || it.type == CommonDataKinds.Event.TYPE_ANNIVERSARY) {
val delimiterType = if (type.isEmpty()) "" else ";$type" val dateTime = it.value.getDateTimeFromDateString()
out.writeLn("$ADR$delimiterType:;;${it.value.replace("\n", "\\n")};;;;") 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) {
contact.events.forEach { card.birthdays.add(Birthday(partialDate))
if (it.type == CommonDataKinds.Event.TYPE_BIRTHDAY) { } else {
out.writeLn("$BDAY${it.value}") 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) { } catch (e: Exception) {
activity.showErrorToast(e) 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) { private fun getPhoneNumberLabel(type: Int) = when (type) {
CommonDataKinds.Phone.TYPE_MOBILE -> CELL CommonDataKinds.Phone.TYPE_MOBILE -> CELL
CommonDataKinds.Phone.TYPE_HOME -> HOME
CommonDataKinds.Phone.TYPE_WORK -> WORK CommonDataKinds.Phone.TYPE_WORK -> WORK
CommonDataKinds.Phone.TYPE_MAIN -> PREF CommonDataKinds.Phone.TYPE_MAIN -> PREF
CommonDataKinds.Phone.TYPE_FAX_WORK -> WORK_FAX CommonDataKinds.Phone.TYPE_FAX_WORK -> WORK_FAX
CommonDataKinds.Phone.TYPE_FAX_HOME -> HOME_FAX CommonDataKinds.Phone.TYPE_FAX_HOME -> HOME_FAX
CommonDataKinds.Phone.TYPE_PAGER -> PAGER CommonDataKinds.Phone.TYPE_PAGER -> PAGER
else -> VOICE else -> HOME
} }
private fun getEmailTypeLabel(type: Int) = when (type) { private fun getEmailTypeLabel(type: Int) = when (type) {
CommonDataKinds.Email.TYPE_HOME -> HOME
CommonDataKinds.Email.TYPE_WORK -> WORK CommonDataKinds.Email.TYPE_WORK -> WORK
CommonDataKinds.Email.TYPE_MOBILE -> MOBILE CommonDataKinds.Email.TYPE_MOBILE -> MOBILE
else -> "" else -> HOME
} }
private fun getAddressTypeLabel(type: Int) = when (type) { private fun getAddressTypeLabel(type: Int) = when (type) {
CommonDataKinds.StructuredPostal.TYPE_HOME -> HOME
CommonDataKinds.StructuredPostal.TYPE_WORK -> WORK CommonDataKinds.StructuredPostal.TYPE_WORK -> WORK
else -> "" else -> HOME
} }
} }

View file

@ -3,8 +3,6 @@ package com.simplemobiletools.contacts.helpers
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.provider.ContactsContract.CommonDataKinds import android.provider.ContactsContract.CommonDataKinds
import android.text.TextUtils
import android.util.Base64
import android.widget.Toast import android.widget.Toast
import com.simplemobiletools.commons.extensions.showErrorToast import com.simplemobiletools.commons.extensions.showErrorToast
import com.simplemobiletools.contacts.activities.SimpleActivity 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.extensions.getCachePhotoUri
import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.* import com.simplemobiletools.contacts.helpers.VcfImporter.ImportResult.*
import com.simplemobiletools.contacts.models.* 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.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.*
class VcfImporter(val activity: SimpleActivity) { class VcfImporter(val activity: SimpleActivity) {
enum class ImportResult { enum class ImportResult {
IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL
} }
private var curPrefix = "" private val PATTERN = "EEE MMM dd HH:mm:ss 'GMT'ZZ YYYY"
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<PhoneNumber>()
private var curEmails = ArrayList<Email>()
private var curEvents = ArrayList<Event>()
private var curAddresses = ArrayList<Address>()
private var curGroups = ArrayList<Group>()
private var curWebsites = ArrayList<String>()
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 var contactsImported = 0 private var contactsImported = 0
private var contactsFailed = 0 private var contactsFailed = 0
@ -63,51 +35,70 @@ class VcfImporter(val activity: SimpleActivity) {
activity.assets.open(path) activity.assets.open(path)
} }
inputStream.bufferedReader().use { val ezContacts = Ezvcard.parse(inputStream).all()
while (true) { for (ezContact in ezContacts) {
val originalLine = it.readLine() ?: break val structuredName = ezContact.structuredName
val line = originalLine.trim() val prefix = structuredName?.prefixes?.firstOrNull() ?: ""
if (line.isEmpty()) { val firstName = structuredName?.given ?: ""
if (isGettingPhoto) { val middleName = structuredName?.additionalNames?.firstOrNull() ?: ""
savePhoto() val surname = structuredName?.family ?: ""
isGettingPhoto = false val suffix = structuredName?.suffixes?.firstOrNull() ?: ""
} val nickname = ezContact.nickname?.values?.firstOrNull() ?: ""
continue val photoUri = ""
} 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
}
when { val phoneNumbers = ArrayList<PhoneNumber>()
line.toUpperCase() == BEGIN_VCARD -> resetValues() ezContact.telephoneNumbers.forEach {
line.toUpperCase().startsWith(NOTE) -> addNotes(line.substring(NOTE.length)) val type = getPhoneNumberTypeId(it.types.firstOrNull()?.value ?: MOBILE)
line.toUpperCase().startsWith(NICKNAME) -> addNickname(line.substring(NICKNAME.length)) val number = it.text
line.toUpperCase().startsWith(N) -> addNames(line.substring(N.length)) phoneNumbers.add(PhoneNumber(number, type))
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)) val emails = ArrayList<Email>()
line.toUpperCase().startsWith(BDAY) -> addBirthday(line.substring(BDAY.length)) ezContact.emails.forEach {
line.toUpperCase().startsWith(ANNIVERSARY) -> addAnniversary(line.substring(ANNIVERSARY.length)) val type = getEmailTypeId(it.types.firstOrNull()?.value ?: HOME)
line.toUpperCase().startsWith(PHOTO) -> addPhoto(line.substring(PHOTO.length)) val email = it.value
line.toUpperCase().startsWith(ORG) -> addCompany(line.substring(ORG.length)) emails.add(Email(email, type))
line.toUpperCase().startsWith(TITLE) -> addJobPosition(line.substring(TITLE.length)) }
line.toUpperCase().startsWith(URL) -> addWebsite(line.substring(URL.length))
line.toUpperCase() == END_VCARD -> saveContact(targetContactSource) val addresses = ArrayList<Address>()
isGettingPhoto -> currentPhotoString.append(line.trim()) 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<Event>()
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<Group>()
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<String>
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) { } catch (e: Exception) {
activity.showErrorToast(e, Toast.LENGTH_LONG) activity.showErrorToast(e, Toast.LENGTH_LONG)
@ -121,238 +112,51 @@ class VcfImporter(val activity: SimpleActivity) {
} }
} }
private fun addNames(names: String) { private fun formatDateToDayCode(date: Date): String {
val parts = names.split(":") val dateTime = DateTime.parse(date.toString(), DateTimeFormat.forPattern(PATTERN))
currentNameIsANSI = parts.first().toUpperCase().contains("QUOTED-PRINTABLE") return dateTime.toString("yyyy-MM-dd")
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 parseNames() { private fun getPhoneNumberTypeId(type: String) = when (type.toUpperCase()) {
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) {
CELL -> CommonDataKinds.Phone.TYPE_MOBILE CELL -> CommonDataKinds.Phone.TYPE_MOBILE
HOME -> CommonDataKinds.Phone.TYPE_HOME HOME -> CommonDataKinds.Phone.TYPE_HOME
WORK -> CommonDataKinds.Phone.TYPE_WORK WORK -> CommonDataKinds.Phone.TYPE_WORK
PREF, MAIN -> CommonDataKinds.Phone.TYPE_MAIN PREF, MAIN -> CommonDataKinds.Phone.TYPE_MAIN
WORK_FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK WORK_FAX -> CommonDataKinds.Phone.TYPE_FAX_WORK
HOME_FAX -> CommonDataKinds.Phone.TYPE_FAX_HOME 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 PAGER -> CommonDataKinds.Phone.TYPE_PAGER
else -> CommonDataKinds.Phone.TYPE_OTHER else -> CommonDataKinds.Phone.TYPE_OTHER
} }
private fun addEmail(email: String) { private fun getEmailTypeId(type: String) = when (type.toUpperCase()) {
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) {
HOME -> CommonDataKinds.Email.TYPE_HOME HOME -> CommonDataKinds.Email.TYPE_HOME
WORK -> CommonDataKinds.Email.TYPE_WORK WORK -> CommonDataKinds.Email.TYPE_WORK
MOBILE -> CommonDataKinds.Email.TYPE_MOBILE MOBILE -> CommonDataKinds.Email.TYPE_MOBILE
else -> CommonDataKinds.Email.TYPE_OTHER else -> CommonDataKinds.Email.TYPE_OTHER
} }
private fun addAddress(address: String) { private fun getAddressTypeId(type: String) = when (type.toUpperCase()) {
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) {
HOME -> CommonDataKinds.Email.TYPE_HOME HOME -> CommonDataKinds.Email.TYPE_HOME
WORK -> CommonDataKinds.Email.TYPE_WORK WORK -> CommonDataKinds.Email.TYPE_WORK
else -> CommonDataKinds.Email.TYPE_OTHER else -> CommonDataKinds.Email.TYPE_OTHER
} }
private fun addBirthday(birthday: String) { private fun savePhoto(byteArray: ByteArray?): String {
curEvents.add(Event(birthday, CommonDataKinds.Event.TYPE_BIRTHDAY)) if (byteArray == null) {
} return ""
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 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 file = activity.getCachePhoto()
val imageAsBytes = Base64.decode(currentPhotoString.toString().toByteArray(), Base64.DEFAULT) val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val bitmap = BitmapFactory.decodeByteArray(imageAsBytes, 0, imageAsBytes.size)
var fileOutputStream: FileOutputStream? = null var fileOutputStream: FileOutputStream? = null
try { try {
fileOutputStream = FileOutputStream(file) fileOutputStream = FileOutputStream(file)
bitmap.compress(currentPhotoCompressionFormat, 100, fileOutputStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
} finally { } finally {
fileOutputStream?.close() fileOutputStream?.close()
} }
curPhotoUri = activity.getCachePhotoUri(file).toString() return 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()
} }
} }

View file

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.2.60' ext.kotlin_version = '1.2.61'
repositories { repositories {
google() google()