Fix abbreviated CJKV names.

This commit is contained in:
Cody Henthorne
2026-09-30 13:34:02 -03:00
committed by Alex Hart
parent 8387e66bb3
commit dde0bce09c
4 changed files with 69 additions and 18 deletions
@@ -14,6 +14,7 @@ import org.signal.core.util.StringUtil
import org.signal.core.util.Util
import org.signal.core.util.UuidUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -177,11 +178,12 @@ object ContactDiscovery {
contactsProvider = {
if (useFullSync) {
Log.d(TAG, "Doing a full system contact sync. There are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter())
SystemContactsRepository.getAllSystemContacts(context, BuildConfig.APPLICATION_ID, phoneNumberFormatter())
} else {
Log.d(TAG, "Doing a partial system contact sync. There are ${result.registeredIds.size} contacts to get info for.")
SystemContactsRepository.getContactDetailsByQueries(
context = context,
ownAccountType = BuildConfig.APPLICATION_ID,
queries = Recipient.resolvedList(result.registeredIds).mapNotNull { it.e164.orElse(null) },
e164Formatter = phoneNumberFormatter()
)
@@ -241,7 +243,7 @@ object ContactDiscovery {
private fun syncRecipientsWithSystemContacts(
context: Context,
rewrites: Map<String, String>,
contactsProvider: () -> ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, phoneNumberFormatter()) },
contactsProvider: () -> ContactIterator = { SystemContactsRepository.getAllSystemContacts(context, BuildConfig.APPLICATION_ID, phoneNumberFormatter()) },
clearInfoForMissingContacts: Boolean
) {
val localNumber: String = SignalStore.account.e164 ?: ""
@@ -36,6 +36,7 @@ class ContactListViewModel(application: Application) : AndroidViewModel(applicat
if (account != null) {
val contactList: List<ContactDetails> = SystemContactsRepository.getAllSystemContacts(
context = application,
ownAccountType = BuildConfig.APPLICATION_ID,
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
).use { it.toList().sortedBy { c -> c.givenName } }
@@ -36,6 +36,7 @@ class ContactLookupViewModel(application: Application) : AndroidViewModel(applic
if (account != null) {
val contactList: List<ContactDetails> = SystemContactsRepository.getContactDetailsByQueries(
context = getApplication(),
ownAccountType = BuildConfig.APPLICATION_ID,
queries = listOf(lookup),
e164Formatter = { number -> PhoneNumberUtils.formatNumberToE164(number, "US") ?: number }
).use { it.toList() }
@@ -8,6 +8,7 @@ import android.content.ContentUris
import android.content.Context
import android.content.OperationApplicationException
import android.database.Cursor
import android.database.sqlite.SQLiteException
import android.net.Uri
import android.os.RemoteException
import android.provider.BaseColumns
@@ -84,6 +85,12 @@ object SystemContactsRepository {
private const val FIELD_TAG = ContactsContract.Data.SYNC2
private const val FIELD_SUPPORTS_VOICE = ContactsContract.RawContacts.SYNC4
private val NAME_SOURCE_COLUMNS = arrayOf(
ContactsContract.RawContacts.ACCOUNT_TYPE,
ContactsContract.Data.RAW_CONTACT_ID,
ContactsContract.Contacts.NAME_RAW_CONTACT_ID
)
/**
* Gets and returns an iterator over data for all contacts, containing both phone number data and structured name data.
*
@@ -91,8 +98,7 @@ object SystemContactsRepository {
* lookup key.
*/
@JvmStatic
fun getAllSystemContacts(context: Context, e164Formatter: (String) -> String?): ContactIterator {
val uri = ContactsContract.Data.CONTENT_URI
fun getAllSystemContacts(context: Context, ownAccountType: String, e164Formatter: (String) -> String?): ContactIterator {
val projection = arrayOf(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER,
@@ -109,13 +115,13 @@ object SystemContactsRepository {
val args = SqlUtil.buildArgs(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
val orderBy = "${ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY} ASC, ${ContactsContract.Data.MIMETYPE} DESC, ${ContactsContract.CommonDataKinds.Phone._ID} DESC"
val cursor: Cursor = context.contentResolver.query(uri, projection, where, args, orderBy) ?: return EmptyContactIterator()
val cursor: Cursor = queryContactData(context, projection, where, args, orderBy) ?: return EmptyContactIterator()
return CursorContactIterator(cursor, e164Formatter)
return CursorContactIterator(cursor, ownAccountType, e164Formatter)
}
@JvmStatic
fun getContactDetailsByQueries(context: Context, queries: List<String>, e164Formatter: (String) -> String?): ContactIterator {
fun getContactDetailsByQueries(context: Context, ownAccountType: String, queries: List<String>, e164Formatter: (String) -> String?): ContactIterator {
val lookupKeys: MutableSet<String> = mutableSetOf()
for (query in queries) {
@@ -134,7 +140,6 @@ object SystemContactsRepository {
return EmptyContactIterator()
}
val uri = ContactsContract.Data.CONTENT_URI
val projection = arrayOf(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.NUMBER,
@@ -154,8 +159,22 @@ object SystemContactsRepository {
val args = lookupKeys.toTypedArray() + SqlUtil.buildArgs(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
val orderBy = "${ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY} ASC, ${ContactsContract.Data.MIMETYPE} DESC, ${ContactsContract.CommonDataKinds.Phone._ID} DESC"
val cursor: Cursor = context.contentResolver.query(uri, projection, where, args, orderBy) ?: return EmptyContactIterator()
return CursorContactIterator(cursor, e164Formatter)
val cursor: Cursor = queryContactData(context, projection, where, args, orderBy) ?: return EmptyContactIterator()
return CursorContactIterator(cursor, ownAccountType, e164Formatter)
}
private fun queryContactData(context: Context, projection: Array<String>, where: String, args: Array<String>, orderBy: String): Cursor? {
val uri = ContactsContract.Data.CONTENT_URI
return try {
context.contentResolver.query(uri, projection + NAME_SOURCE_COLUMNS, where, args, orderBy)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Name source columns rejected, querying without them.", e)
context.contentResolver.query(uri, projection, where, args, orderBy)
} catch (e: SQLiteException) {
Log.w(TAG, "Name source columns rejected, querying without them.", e)
context.contentResolver.query(uri, projection, where, args, orderBy)
}
}
/**
@@ -831,14 +850,17 @@ object SystemContactsRepository {
* - Assume you're already on the correct row at the start of [next].
* - Store the lookup key from the first row.
* - Read all phone entries for that lookup key and store them.
* - Read the first name entry for that lookup key and store it.
* - Read the best name entry for that lookup key and store it. See [readStructuredName].
* - Skip all other rows for that lookup key. This will ensure that you're on the correct row for the next call to [next]
*/
private class CursorContactIterator(
private val cursor: Cursor,
private val ownAccountType: String,
private val e164Formatter: (String) -> String?
) : ContactIterator {
private val hasNameSourceColumns: Boolean = cursor.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID) >= 0
init {
cursor.moveToFirst()
}
@@ -905,15 +927,40 @@ object SystemContactsRepository {
.toList()
}
/**
* Skips our own name rows since the provider re-splits them on display name updates. Returns null if only ours exist, and callers then use the display name.
*/
fun readStructuredName(cursor: Cursor, lookupKey: String): StructuredName? {
return if (!cursor.isAfterLast && cursor.getLookupKey() == lookupKey && cursor.isNameMimeType()) {
StructuredName(
givenName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME),
familyName = cursor.requireString(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
)
} else {
null
if (!hasNameSourceColumns) {
return if (!cursor.isAfterLast && cursor.getLookupKey() == lookupKey && cursor.isNameMimeType()) cursor.toStructuredName() else null
}
var firstFound: StructuredName? = null
while (!cursor.isAfterLast && cursor.getLookupKey() == lookupKey && cursor.isNameMimeType()) {
if (cursor.requireString(ContactsContract.RawContacts.ACCOUNT_TYPE) != ownAccountType) {
val name = cursor.toStructuredName()
if (cursor.requireLong(ContactsContract.Data.RAW_CONTACT_ID) == cursor.requireLong(ContactsContract.Contacts.NAME_RAW_CONTACT_ID)) {
return name
}
if (firstFound == null) {
firstFound = name
}
}
cursor.moveToNext()
}
return firstFound
}
private fun Cursor.toStructuredName(): StructuredName {
return StructuredName(
givenName = requireString(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME),
familyName = requireString(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME)
)
}
fun Cursor.getLookupKey(): String {