From ecdbc9036aae0ef00b609ca15283b56d023d9fd0 Mon Sep 17 00:00:00 2001 From: Naveen Date: Thu, 3 Nov 2022 00:01:52 +0530 Subject: [PATCH] Extract all properties when adding contact attachment --- .../smsmessenger/activities/ThreadActivity.kt | 57 +-- .../adapters/AttachmentsAdapter.kt | 11 +- .../smsmessenger/helpers/ContactsHelper.kt | 449 ++++++++++++++++++ app/src/main/res/xml/provider_paths.xml | 2 +- 4 files changed, 486 insertions(+), 33 deletions(-) create mode 100644 app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt index 02667c6b..466f13ca 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/activities/ThreadActivity.kt @@ -57,9 +57,6 @@ import com.simplemobiletools.smsmessenger.dialogs.ScheduleMessageDialog import com.simplemobiletools.smsmessenger.extensions.* import com.simplemobiletools.smsmessenger.helpers.* import com.simplemobiletools.smsmessenger.models.* -import ezvcard.VCard -import ezvcard.property.FormattedName -import ezvcard.property.Telephone import kotlinx.android.synthetic.main.activity_thread.* import kotlinx.android.synthetic.main.item_selected_contact.view.* import org.greenrobot.eventbus.EventBus @@ -240,7 +237,7 @@ class ThreadActivity : SimpleActivity() { addAttachment(data) } PICK_CONTACT_INTENT -> if (data != null) { - handleContactAttachment(data) + addContactAttachment(data) } PICK_SAVE_FILE_INTENT -> if (data != null) { saveAttachment(resultData) @@ -817,17 +814,16 @@ class ThreadActivity : SimpleActivity() { } } - private fun createTemporaryFile(extension: String = ".jpg"): File { - val outputDirectory = File(cacheDir, "captured").apply { + private fun getAttachmentsDir(): File { + return File(cacheDir, "attachments").apply { if (!exists()) { mkdirs() } } - return File.createTempFile("attachment_", extension, outputDirectory) } private fun launchCapturePhotoIntent() { - val imageFile = createTemporaryFile() + val imageFile = File.createTempFile("attachment_", ".jpg", getAttachmentsDir()) capturedImageUri = getMyFileUri(imageFile) val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri) @@ -874,26 +870,30 @@ class ThreadActivity : SimpleActivity() { launchActivityForResult(intent, PICK_CONTACT_INTENT) } - private fun handleContactAttachment(contactUri: Uri) { - val projection = arrayOf( - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.NUMBER - ) - queryCursor(contactUri, projection, null, null, null) { cursor -> - if (cursor.moveToFirst()) { - val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME) - val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) - val name = cursor.getString(nameIndex) - val number = cursor.getString(numberIndex) - // todo: export all properties using VcfExporter - val vCard = VCard() - vCard.addTelephoneNumber(Telephone(number)) - vCard.addFormattedName(FormattedName(name)) - val file = createTemporaryFile(".vcf") - val outputStream = file.outputStream() - vCard.write(outputStream) - val vCardUri = getMyFileUri(file) - addAttachment(vCardUri) + private fun addContactAttachment(contactUri: Uri) { + ensureBackgroundThread { + val contact = ContactsHelper(this).getContactFromUri(contactUri) + if (contact != null) { + val outputFile = File(getAttachmentsDir(), "${contact.contactId}.vcf") + val outputStream = outputFile.outputStream() + + VcfExporter().exportContacts( + activity = this, + outputStream = outputStream, + contacts = arrayListOf(contact), + showExportingToast = false, + ) { + if (it == VcfExporter.ExportResult.EXPORT_OK) { + val vCardUri = getMyFileUri(outputFile) + runOnUiThread { + addAttachment(vCardUri) + } + } else { + toast(R.string.unknown_error_occurred) + } + } + } else { + toast(R.string.unknown_error_occurred) } } } @@ -914,6 +914,7 @@ class ThreadActivity : SimpleActivity() { if (adapter == null) { adapter = AttachmentsAdapter( activity = this, + recyclerView = thread_attachments_recyclerview, onItemClick = {}, onAttachmentsRemoved = { thread_attachments_recyclerview.beGone() diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt index 37baa75b..135da40b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/adapters/AttachmentsAdapter.kt @@ -31,6 +31,7 @@ import kotlinx.android.synthetic.main.item_remove_attachment_button.view.* class AttachmentsAdapter( val activity: BaseSimpleActivity, + val recyclerView: RecyclerView, val onItemClick: (AttachmentSelection) -> Unit, val onAttachmentsRemoved: () -> Unit, val onReady: (() -> Unit) @@ -45,8 +46,10 @@ class AttachmentsAdapter( fun clear() { attachments.clear() - submitList(ArrayList()) - onAttachmentsRemoved() + submitList(emptyList()) + recyclerView.onGlobalLayout { + onAttachmentsRemoved() + } } fun addAttachment(attachment: AttachmentSelection) { @@ -58,9 +61,9 @@ class AttachmentsAdapter( private fun removeAttachment(attachment: AttachmentSelection) { attachments.removeAll { AttachmentSelection.areItemsTheSame(it, attachment) } if (attachments.isEmpty()) { - onAttachmentsRemoved() + clear() } else { - submitList(ArrayList(attachments)) + submitList(attachments.toList()) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt new file mode 100644 index 00000000..36c9d123 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/smsmessenger/helpers/ContactsHelper.kt @@ -0,0 +1,449 @@ +package com.simplemobiletools.smsmessenger.helpers + +import android.content.Context +import android.net.Uri +import android.provider.ContactsContract +import android.provider.ContactsContract.CommonDataKinds.* +import android.provider.ContactsContract.Data +import android.text.TextUtils +import android.util.SparseArray +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.models.PhoneNumber +import com.simplemobiletools.commons.models.contacts.* +import com.simplemobiletools.commons.models.contacts.Email +import com.simplemobiletools.commons.models.contacts.Event +import com.simplemobiletools.commons.models.contacts.Organization +import com.simplemobiletools.commons.overloads.times + +// based on the ContactsHelper from Simple-Contacts +class ContactsHelper(val context: Context) { + private var displayContactSources = ArrayList() + + fun getContactFromUri(uri: Uri): Contact? { + val projection = arrayOf(Data.RAW_CONTACT_ID) + val cursor = context.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + if (cursor.moveToFirst()) { + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + return getContactWithId(id) + } + } + return null + } + + private fun getContactWithId(id: Int): Contact? { + if (id == 0) { + return null + } + + val selection = "(${Data.MIMETYPE} = ? OR ${Data.MIMETYPE} = ?) AND ${Data.RAW_CONTACT_ID} = ?" + val selectionArgs = arrayOf(StructuredName.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, id.toString()) + return parseContactCursor(selection, selectionArgs) + } + + private fun parseContactCursor(selection: String, selectionArgs: Array): Contact? { + val storedGroups = getDeviceStoredGroups() + val uri = Data.CONTENT_URI + val projection = getContactProjection() + + val cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) + cursor?.use { + if (cursor.moveToFirst()) { + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + + var prefix = "" + var firstName = "" + var middleName = "" + var surname = "" + var suffix = "" + val mimetype = cursor.getStringValue(Data.MIMETYPE) + + // ignore names at Organization type contacts + if (mimetype == StructuredName.CONTENT_ITEM_TYPE) { + prefix = cursor.getStringValue(StructuredName.PREFIX) ?: "" + firstName = cursor.getStringValue(StructuredName.GIVEN_NAME) ?: "" + middleName = cursor.getStringValue(StructuredName.MIDDLE_NAME) ?: "" + surname = cursor.getStringValue(StructuredName.FAMILY_NAME) ?: "" + suffix = cursor.getStringValue(StructuredName.SUFFIX) ?: "" + } + + val nickname = getNicknames(id)[id] ?: "" + val photoUri = cursor.getStringValue(Phone.PHOTO_URI) ?: "" + val number = getPhoneNumbers(id)[id] ?: ArrayList() + val emails = getEmails(id)[id] ?: ArrayList() + val addresses = getAddresses(id)[id] ?: ArrayList() + val events = getEvents(id)[id] ?: ArrayList() + val notes = getNotes(id)[id] ?: "" + val accountName = cursor.getStringValue(ContactsContract.RawContacts.ACCOUNT_NAME) ?: "" + val starred = cursor.getIntValue(StructuredName.STARRED) + val ringtone = cursor.getStringValue(StructuredName.CUSTOM_RINGTONE) + val contactId = cursor.getIntValue(Data.CONTACT_ID) + val groups = getContactGroups(storedGroups, contactId)[contactId] ?: ArrayList() + val thumbnailUri = cursor.getStringValue(StructuredName.PHOTO_THUMBNAIL_URI) ?: "" + val organization = getOrganizations(id)[id] ?: Organization("", "") + val websites = getWebsites(id)[id] ?: ArrayList() + val ims = getIMs(id)[id] ?: ArrayList() + return Contact( + id, prefix, firstName, middleName, surname, suffix, nickname, photoUri, number, emails, addresses, events, + accountName, starred, contactId, thumbnailUri, null, notes, groups, organization, websites, ims, mimetype, ringtone + ) + } + } + + return null + } + + private fun getPhoneNumbers(contactId: Int? = null): SparseArray> { + val phoneNumbers = SparseArray>() + val uri = Phone.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + Phone.NUMBER, + Phone.NORMALIZED_NUMBER, + Phone.TYPE, + Phone.LABEL, + Phone.IS_PRIMARY + ) + + val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?" + val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString()) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val number = cursor.getStringValue(Phone.NUMBER) ?: return@queryCursor + val normalizedNumber = cursor.getStringValue(Phone.NORMALIZED_NUMBER) ?: number.normalizePhoneNumber() + val type = cursor.getIntValue(Phone.TYPE) + val label = cursor.getStringValue(Phone.LABEL) ?: "" + val isPrimary = cursor.getIntValue(Phone.IS_PRIMARY) != 0 + + if (phoneNumbers[id] == null) { + phoneNumbers.put(id, ArrayList()) + } + + val phoneNumber = PhoneNumber(number, type, label, normalizedNumber, isPrimary) + phoneNumbers[id].add(phoneNumber) + } + + return phoneNumbers + } + + private fun getNicknames(contactId: Int? = null): SparseArray { + val nicknames = SparseArray() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + Nickname.NAME + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(Nickname.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val nickname = cursor.getStringValue(Nickname.NAME) ?: return@queryCursor + nicknames.put(id, nickname) + } + + return nicknames + } + + private fun getEmails(contactId: Int? = null): SparseArray> { + val emails = SparseArray>() + val uri = ContactsContract.CommonDataKinds.Email.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + ContactsContract.CommonDataKinds.Email.DATA, + ContactsContract.CommonDataKinds.Email.TYPE, + ContactsContract.CommonDataKinds.Email.LABEL + ) + + val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?" + val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString()) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val email = cursor.getStringValue(ContactsContract.CommonDataKinds.Email.DATA) ?: return@queryCursor + val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Email.TYPE) + val label = cursor.getStringValue(ContactsContract.CommonDataKinds.Email.LABEL) ?: "" + + if (emails[id] == null) { + emails.put(id, ArrayList()) + } + + emails[id]!!.add(Email(email, type, label)) + } + + return emails + } + + private fun getAddresses(contactId: Int? = null): SparseArray> { + val addresses = SparseArray>() + val uri = StructuredPostal.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + StructuredPostal.FORMATTED_ADDRESS, + StructuredPostal.TYPE, + StructuredPostal.LABEL + ) + + val selection = if (contactId == null) getSourcesSelection() else "${Data.RAW_CONTACT_ID} = ?" + val selectionArgs = if (contactId == null) getSourcesSelectionArgs() else arrayOf(contactId.toString()) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val address = cursor.getStringValue(StructuredPostal.FORMATTED_ADDRESS) ?: return@queryCursor + val type = cursor.getIntValue(StructuredPostal.TYPE) + val label = cursor.getStringValue(StructuredPostal.LABEL) ?: "" + + if (addresses[id] == null) { + addresses.put(id, ArrayList()) + } + + addresses[id]!!.add(Address(address, type, label)) + } + + return addresses + } + + private fun getIMs(contactId: Int? = null): SparseArray> { + val IMs = SparseArray>() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + Im.DATA, + Im.PROTOCOL, + Im.CUSTOM_PROTOCOL + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(Im.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val IM = cursor.getStringValue(Im.DATA) ?: return@queryCursor + val type = cursor.getIntValue(Im.PROTOCOL) + val label = cursor.getStringValue(Im.CUSTOM_PROTOCOL) ?: "" + + if (IMs[id] == null) { + IMs.put(id, ArrayList()) + } + + IMs[id]!!.add(IM(IM, type, label)) + } + + return IMs + } + + private fun getEvents(contactId: Int? = null): SparseArray> { + val events = SparseArray>() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + ContactsContract.CommonDataKinds.Event.START_DATE, + ContactsContract.CommonDataKinds.Event.TYPE + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val startDate = cursor.getStringValue(ContactsContract.CommonDataKinds.Event.START_DATE) ?: return@queryCursor + val type = cursor.getIntValue(ContactsContract.CommonDataKinds.Event.TYPE) + + if (events[id] == null) { + events.put(id, ArrayList()) + } + + events[id]!!.add(Event(startDate, type)) + } + + return events + } + + private fun getNotes(contactId: Int? = null): SparseArray { + val notes = SparseArray() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + Note.NOTE + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(Note.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val note = cursor.getStringValue(Note.NOTE) ?: return@queryCursor + notes.put(id, note) + } + + return notes + } + + private fun getOrganizations(contactId: Int? = null): SparseArray { + val organizations = SparseArray() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + ContactsContract.CommonDataKinds.Organization.COMPANY, + ContactsContract.CommonDataKinds.Organization.TITLE + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val company = cursor.getStringValue(ContactsContract.CommonDataKinds.Organization.COMPANY) ?: "" + val title = cursor.getStringValue(ContactsContract.CommonDataKinds.Organization.TITLE) ?: "" + if (company.isEmpty() && title.isEmpty()) { + return@queryCursor + } + + val organization = Organization(company, title) + organizations.put(id, organization) + } + + return organizations + } + + private fun getWebsites(contactId: Int? = null): SparseArray> { + val websites = SparseArray>() + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.RAW_CONTACT_ID, + Website.URL + ) + + val selection = getSourcesSelection(true, contactId != null) + val selectionArgs = getSourcesSelectionArgs(Website.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.RAW_CONTACT_ID) + val url = cursor.getStringValue(Website.URL) ?: return@queryCursor + + if (websites[id] == null) { + websites.put(id, ArrayList()) + } + + websites[id]!!.add(url) + } + + return websites + } + + private fun getDeviceStoredGroups(): java.util.ArrayList { + val groups = java.util.ArrayList() + + val uri = ContactsContract.Groups.CONTENT_URI + val projection = arrayOf( + ContactsContract.Groups._ID, + ContactsContract.Groups.TITLE, + ContactsContract.Groups.SYSTEM_ID + ) + + val selection = "${ContactsContract.Groups.AUTO_ADD} = ? AND ${ContactsContract.Groups.FAVORITES} = ?" + val selectionArgs = arrayOf("0", "0") + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getLongValue(ContactsContract.Groups._ID) + val title = cursor.getStringValue(ContactsContract.Groups.TITLE) ?: return@queryCursor + + val systemId = cursor.getStringValue(ContactsContract.Groups.SYSTEM_ID) + if (groups.map { it.title }.contains(title) && systemId != null) { + return@queryCursor + } + + groups.add(Group(id, title)) + } + return groups + } + + private fun getContactGroups(storedGroups: ArrayList, contactId: Int? = null): SparseArray> { + val groups = SparseArray>() + + val uri = Data.CONTENT_URI + val projection = arrayOf( + Data.CONTACT_ID, + Data.DATA1 + ) + + val selection = getSourcesSelection(true, contactId != null, false) + val selectionArgs = getSourcesSelectionArgs(GroupMembership.CONTENT_ITEM_TYPE, contactId) + + context.queryCursor(uri, projection, selection, selectionArgs, showErrors = true) { cursor -> + val id = cursor.getIntValue(Data.CONTACT_ID) + val newRowId = cursor.getLongValue(Data.DATA1) + + val groupTitle = storedGroups.firstOrNull { it.id == newRowId }?.title ?: return@queryCursor + val group = Group(newRowId, groupTitle) + if (groups[id] == null) { + groups.put(id, ArrayList()) + } + groups[id]!!.add(group) + } + + return groups + } + + private fun getQuestionMarks() = ("?," * displayContactSources.filter { it.isNotEmpty() }.size).trimEnd(',') + + private fun getSourcesSelection(addMimeType: Boolean = false, addContactId: Boolean = false, useRawContactId: Boolean = true): String { + val strings = ArrayList() + if (addMimeType) { + strings.add("${Data.MIMETYPE} = ?") + } + + if (addContactId) { + strings.add("${if (useRawContactId) Data.RAW_CONTACT_ID else Data.CONTACT_ID} = ?") + } else { + // sometimes local device storage has null account_name, handle it properly + val accountNameString = StringBuilder() + if (displayContactSources.contains("")) { + accountNameString.append("(") + } + accountNameString.append("${ContactsContract.RawContacts.ACCOUNT_NAME} IN (${getQuestionMarks()})") + if (displayContactSources.contains("")) { + accountNameString.append(" OR ${ContactsContract.RawContacts.ACCOUNT_NAME} IS NULL)") + } + strings.add(accountNameString.toString()) + } + + return TextUtils.join(" AND ", strings) + } + + private fun getSourcesSelectionArgs(mimetype: String? = null, contactId: Int? = null): Array { + val args = ArrayList() + + if (mimetype != null) { + args.add(mimetype) + } + + if (contactId != null) { + args.add(contactId.toString()) + } else { + args.addAll(displayContactSources.filter { it.isNotEmpty() }) + } + + return args.toTypedArray() + } + + private fun getContactProjection() = arrayOf( + Data.MIMETYPE, + Data.CONTACT_ID, + Data.RAW_CONTACT_ID, + StructuredName.PREFIX, + StructuredName.GIVEN_NAME, + StructuredName.MIDDLE_NAME, + StructuredName.FAMILY_NAME, + StructuredName.SUFFIX, + StructuredName.PHOTO_URI, + StructuredName.PHOTO_THUMBNAIL_URI, + StructuredName.STARRED, + StructuredName.CUSTOM_RINGTONE, + ContactsContract.RawContacts.ACCOUNT_NAME, + ContactsContract.RawContacts.ACCOUNT_TYPE + ) + +} diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 758c02cd..7c909b1b 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,5 +1,5 @@ - +