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