From 0c8203cb35c5eaaf7da16309c596cc26378ac702 Mon Sep 17 00:00:00 2001 From: mhwazrah Date: Sun, 24 May 2026 01:56:27 +0300 Subject: [PATCH 1/4] Improve T9 contact search for non-Latin names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the offset bug in ContactsAdapter.highlightTextFromNumbers that mis-positioned the colored span on Arabic names so the wrong word was highlighted. Make the highlight span work across spaces by comparing against space-stripped digits and mapping the match back to the original characters, so searching 9878685 highlights عمي محمد across the gap. Sort filtered results by starts-with match first, then by call frequency from the recent call log, then alphabetical, so names beginning with the typed digits and contacts you call most often surface to the top. --- .../dialer/activities/DialpadActivity.kt | 28 +++++++++++++------ .../goodwy/dialer/adapters/ContactsAdapter.kt | 23 +++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt index f7289f60..504b9ebc 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt @@ -1152,13 +1152,18 @@ class DialpadActivity : SimpleActivity() { val text = if (config.formatPhoneNumbers) textFormat.removeNumberFormatting() else textFormat + val langPref = config.dialpadSecondaryLanguage ?: "" + val langLocale = Locale.getDefault().language + val isAutoLang = DialpadT9.getSupportedSecondaryLanguages().contains(langLocale) && langPref == LANGUAGE_SYSTEM + val lang = if (isAutoLang) langLocale else langPref + val collator = Collator.getInstance(sysLocale()) + val callCounts = HashMap() + allRecentCalls.forEach { call -> + val key = call.phoneNumber.normalizePhoneNumber() + if (key.isNotEmpty()) callCounts[key] = (callCounts[key] ?: 0) + 1 + } val filtered = allContacts.filter { contact -> - val langPref = config.dialpadSecondaryLanguage ?: "" - val langLocale = Locale.getDefault().language - val isAutoLang = DialpadT9.getSupportedSecondaryLanguages().contains(langLocale) && langPref == LANGUAGE_SYSTEM - val lang = if (isAutoLang) langLocale else langPref - val convertedName = DialpadT9.convertLettersToNumbers( contact.name.normalizeString().uppercase(), lang) val convertedNameWithoutSpaces = convertedName.filterNot { it.isWhitespace() } @@ -1177,9 +1182,16 @@ class DialpadActivity : SimpleActivity() { || (convertedNameToDisplayWithoutSpaces.contains(text, true)) || (convertedNickname.contains(text, true)) || (convertedCompany.contains(text, true)) - }.sortedWith(compareBy(collator) { - it.getNameToDisplay() - }).toMutableList() as ArrayList + }.sortedWith( + compareByDescending { + DialpadT9.convertLettersToNumbers( + it.getNameToDisplay().normalizeString().uppercase(), lang) + .filterNot { c -> c.isWhitespace() } + .startsWith(text) + }.thenByDescending { contact -> + contact.phoneNumbers.maxOfOrNull { callCounts[it.value.normalizePhoneNumber()] ?: 0 } ?: 0 + }.thenBy(collator) { it.getNameToDisplay() } + ).toMutableList() as ArrayList // binding.letterFastscroller.setupWithContacts(binding.dialpadList, filtered) diff --git a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt index b84ab69e..4a2f0f29 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt @@ -406,14 +406,21 @@ class ContactsAdapter( private fun String.highlightTextFromNumbers(textToHighlight: String, primaryColor: Int, language: String): SpannableString { val spannableString = SpannableString(this) val digits = DialpadT9.convertLettersToNumbers(this.uppercase(), language) - if (digits.contains(textToHighlight)) { - //offsetting the characters to be extracted, by the number of first non-letter or non-numeric characters. - val lettersAndNumbers = Regex("[^A-Za-z0-9 ]") - val firstSymbol = lettersAndNumbers.replace(this, "").firstOrNull() - val offsetIndex = if (firstSymbol != null) this.indexOf(firstSymbol, 0, true) else 0 - - val startIndex = digits.indexOf(textToHighlight, 0, true) + offsetIndex - val endIndex = (startIndex + textToHighlight.length).coerceAtMost(length) + + val compressed = StringBuilder(digits.length) + val originalPos = IntArray(digits.length) + var written = 0 + digits.forEachIndexed { i, c -> + if (!c.isWhitespace()) { + compressed.append(c) + originalPos[written++] = i + } + } + val start = compressed.indexOf(textToHighlight, 0, ignoreCase = true) + if (start >= 0) { + val startIndex = originalPos[start] + val endCompressed = (start + textToHighlight.length - 1).coerceAtMost(written - 1) + val endIndex = (originalPos[endCompressed] + 1).coerceAtMost(length) try { spannableString.setSpan(ForegroundColorSpan(primaryColor), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_INCLUSIVE) } catch (_: IndexOutOfBoundsException) { From b2012cf2b72e967e3fa2cadd71859c9a2aa79d32 Mon Sep 17 00:00:00 2001 From: mhwazrah Date: Sun, 24 May 2026 13:58:10 +0300 Subject: [PATCH 2/4] Match T9 search on digit-only converted names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The filter ran contact names through normalizeString which NFD decomposes hamza-alef variants like أ into ا plus a combining hamza U+0654. The strip regex only covers U+0300 to U+036F so the combining mark survives and ends up as a non-digit character inside the converted-digits string, breaking the contiguous substring match. Contacts like خالد (أبو سعود) disappeared from results. Switching the without-spaces variant to a digits-only variant filters out combining marks, parens and any other non-digit noise in one go, since the typed T9 query is always digits anyway. The sort tier and highlighter pick up the same change so highlighting stays in sync. Nickname and company also gain the digits-only variant they were previously missing. --- .../goodwy/dialer/activities/DialpadActivity.kt | 14 +++++++++----- .../com/goodwy/dialer/adapters/ContactsAdapter.kt | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt index 504b9ebc..ed587511 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt @@ -1166,27 +1166,31 @@ class DialpadActivity : SimpleActivity() { val filtered = allContacts.filter { contact -> val convertedName = DialpadT9.convertLettersToNumbers( contact.name.normalizeString().uppercase(), lang) - val convertedNameWithoutSpaces = convertedName.filterNot { it.isWhitespace() } + val convertedNameDigitsOnly = convertedName.filter { it.isDigit() } val convertedNickname = DialpadT9.convertLettersToNumbers( contact.nickname.normalizeString().uppercase(), lang) + val convertedNicknameDigitsOnly = convertedNickname.filter { it.isDigit() } val convertedCompany = DialpadT9.convertLettersToNumbers( contact.organization.company.normalizeString().uppercase(), lang) + val convertedCompanyDigitsOnly = convertedCompany.filter { it.isDigit() } val convertedNameToDisplay = DialpadT9.convertLettersToNumbers( contact.getNameToDisplay().normalizeString().uppercase(), lang) - val convertedNameToDisplayWithoutSpaces = convertedNameToDisplay.filterNot { it.isWhitespace() } + val convertedNameToDisplayDigitsOnly = convertedNameToDisplay.filter { it.isDigit() } contact.doesContainPhoneNumber(text, convertLetters = true, search = true) || (convertedName.contains(text, true)) - || (convertedNameWithoutSpaces.contains(text, true)) + || (convertedNameDigitsOnly.contains(text, true)) || (convertedNameToDisplay.contains(text, true)) - || (convertedNameToDisplayWithoutSpaces.contains(text, true)) + || (convertedNameToDisplayDigitsOnly.contains(text, true)) || (convertedNickname.contains(text, true)) + || (convertedNicknameDigitsOnly.contains(text, true)) || (convertedCompany.contains(text, true)) + || (convertedCompanyDigitsOnly.contains(text, true)) }.sortedWith( compareByDescending { DialpadT9.convertLettersToNumbers( it.getNameToDisplay().normalizeString().uppercase(), lang) - .filterNot { c -> c.isWhitespace() } + .filter { c -> c.isDigit() } .startsWith(text) }.thenByDescending { contact -> contact.phoneNumbers.maxOfOrNull { callCounts[it.value.normalizePhoneNumber()] ?: 0 } ?: 0 diff --git a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt index 4a2f0f29..4019517c 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt @@ -411,7 +411,7 @@ class ContactsAdapter( val originalPos = IntArray(digits.length) var written = 0 digits.forEachIndexed { i, c -> - if (!c.isWhitespace()) { + if (c.isDigit()) { compressed.append(c) originalPos[written++] = i } From 1922d0c0e851a252d8247cb2084f11847ebf2c12 Mon Sep 17 00:00:00 2001 From: mhwazrah Date: Sun, 24 May 2026 14:09:27 +0300 Subject: [PATCH 3/4] Make call frequency the primary T9 sort tier Swap the order so the most-called contact in the result list comes first, then names that start with the typed digits as a tiebreaker, then alphabetical. Previously starts-with was the primary tier so a contact with a leading match always outranked a more frequently called contact whose match was elsewhere in the name, which felt backward when the heavily called number was a few rows down. --- .../kotlin/com/goodwy/dialer/activities/DialpadActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt index ed587511..c77d0ccb 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/activities/DialpadActivity.kt @@ -1187,13 +1187,13 @@ class DialpadActivity : SimpleActivity() { || (convertedCompany.contains(text, true)) || (convertedCompanyDigitsOnly.contains(text, true)) }.sortedWith( - compareByDescending { + compareByDescending { contact -> + contact.phoneNumbers.maxOfOrNull { callCounts[it.value.normalizePhoneNumber()] ?: 0 } ?: 0 + }.thenByDescending { DialpadT9.convertLettersToNumbers( it.getNameToDisplay().normalizeString().uppercase(), lang) .filter { c -> c.isDigit() } .startsWith(text) - }.thenByDescending { contact -> - contact.phoneNumbers.maxOfOrNull { callCounts[it.value.normalizePhoneNumber()] ?: 0 } ?: 0 }.thenBy(collator) { it.getNameToDisplay() } ).toMutableList() as ArrayList From f4ce4091e69d0814d40160f9ca59dcc810887f5a Mon Sep 17 00:00:00 2001 From: mhwazrah Date: Sun, 24 May 2026 20:57:23 +0300 Subject: [PATCH 4/4] Stop injecting spaces into the T9 highlight query Contacts with an Arabic first name followed by a Latin surname (for example Ahmad Smith) were not highlighting the Latin part on T9 search. Searching 624 to find Smi listed the contact but no characters were colored. The outer code in ContactsAdapter pre-processed the search text by detecting the space between the Arabic and Latin parts and inserting a literal space into the digit string ("624" became "6 24"), which used to align with the with-spaces digits the old highlighter searched. The new digit-only highlighter compresses spaces out before indexOf, so "6 24" no longer matches anything. Removing the preprocessing lets the highlighter do its own cross-space matching and the highlight lands on Smi as expected. --- .../goodwy/dialer/adapters/ContactsAdapter.kt | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt index 4019517c..b6928b58 100755 --- a/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt +++ b/app/src/main/kotlin/com/goodwy/dialer/adapters/ContactsAdapter.kt @@ -465,25 +465,11 @@ class ContactsAdapter( if (normalizedName.contains(normalizedSearchText, true)) { name.highlightTextPart(normalizedSearchText, properPrimaryColor) } else { - var spacedTextToHighlight = textToHighlight - val strippedName = PhoneNumberUtils.convertKeypadLettersToDigits(name.filterNot { it.isWhitespace() }) - val startIndex = strippedName.indexOf(textToHighlight) - - if (strippedName.contains(textToHighlight) && strippedName != name) { - for (i in 0..spacedTextToHighlight.length) { - if (name.toCharArray().size > startIndex + i) { - if (name[startIndex + i].isWhitespace()) { - spacedTextToHighlight = spacedTextToHighlight.replaceRange(i, i, " ") - } - } - } - } - val langPref = activity.config.dialpadSecondaryLanguage ?: "" val langLocale = Locale.getDefault().language val isAutoLang = DialpadT9.getSupportedSecondaryLanguages().contains(langLocale) && langPref == LANGUAGE_SYSTEM val lang = if (isAutoLang) langLocale else langPref - name.highlightTextFromNumbers(spacedTextToHighlight, properPrimaryColor, lang) + name.highlightTextFromNumbers(textToHighlight, properPrimaryColor, lang) } } }