rewriting contact exporting/importing, rely on vcard at parsing
This commit is contained in:
parent
389d6e9266
commit
c0720e3e73
10 changed files with 215 additions and 439 deletions
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -73,8 +73,8 @@ class MainActivity : SimpleActivity(), RefreshContactsListener {
|
|||
} else {
|
||||
checkContactPermissions()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
storeStateVariables()
|
||||
checkWhatsNewDialog()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<VCard>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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 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<PhoneNumber>()
|
||||
ezContact.telephoneNumbers.forEach {
|
||||
val type = getPhoneNumberTypeId(it.types.firstOrNull()?.value ?: MOBILE)
|
||||
val number = it.text
|
||||
phoneNumbers.add(PhoneNumber(number, type))
|
||||
}
|
||||
|
||||
val emails = ArrayList<Email>()
|
||||
ezContact.emails.forEach {
|
||||
val type = getEmailTypeId(it.types.firstOrNull()?.value ?: HOME)
|
||||
val email = it.value
|
||||
emails.add(Email(email, type))
|
||||
}
|
||||
|
||||
val addresses = ArrayList<Address>()
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue