Use better country code fallback in regV5.

This commit is contained in:
Greyson Parrelli
2026-07-15 12:55:22 -04:00
parent 06e7b79e3d
commit 6a18bcc915
3 changed files with 133 additions and 7 deletions
@@ -5,9 +5,13 @@
package org.signal.registration
import android.Manifest
import android.annotation.SuppressLint
import android.app.backup.BackupManager
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import androidx.core.content.ContextCompat
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.i18n.phonenumbers.PhoneNumberUtil
import kotlinx.coroutines.Dispatchers
@@ -54,6 +58,7 @@ import org.signal.registration.proto.AccountData
import org.signal.registration.proto.LinkedDeviceData
import org.signal.registration.proto.ProvisioningData
import org.signal.registration.proto.SvrCredential
import org.signal.registration.screens.countrycode.CountryUtils
import org.signal.registration.screens.localbackuprestore.LocalBackupInfo
import org.signal.registration.screens.messagesync.LinkAndSyncProgress
import org.signal.registration.screens.remotebackuprestore.RemoteBackupRestoreProgress
@@ -175,15 +180,41 @@ class RegistrationRepository(val context: Context, val networkController: Networ
}
}
/**
* Determines the region code to default the country picker to. In priority order:
* 1. The region of the device's own phone number, if the phone permission is granted and the number is readable.
* 2. The network operator's country.
* 3. The SIM's home country.
* 4. A best-guess region derived from the device locale.
* 5. US, as a last resort.
*/
fun getDefaultRegionCode(): String {
val maybeRegionCode = Util.getNetworkCountryIso(context)
val maybeCountryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(maybeRegionCode)
return if (maybeRegionCode != null && maybeCountryCode != 0) {
maybeRegionCode
} else {
Log.w(TAG, "Invalid region or country code. Defaulting to US.")
"US"
return deviceNumberRegionCode()
?: Util.getNetworkCountryIso(context).takeIfValidRegion()
?: Util.getSimCountryIso(context).orElse(null).takeIfValidRegion()
?: CountryUtils.localeToRegionCode(Locale.getDefault()).takeIfValidRegion()
?: run {
Log.w(TAG, "No usable region from telephony or locale. Defaulting to US.")
"US"
}
}
@SuppressLint("MissingPermission")
private fun deviceNumberRegionCode(): String? {
val hasPhonePermission = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED
if (!hasPhonePermission) {
return null
}
val deviceNumber = Util.getDeviceNumber(context).orElse(null) ?: return null
val phoneNumberUtil = PhoneNumberUtil.getInstance()
return (phoneNumberUtil.getRegionCodeForNumber(deviceNumber) ?: phoneNumberUtil.getRegionCodeForCountryCode(deviceNumber.countryCode)).takeIfValidRegion()
}
private fun String?.takeIfValidRegion(): String? {
return this?.takeIf { it.isNotEmpty() && PhoneNumberUtil.getInstance().getCountryCodeForRegion(it) != 0 }
}
suspend fun getRestoredSvrCredentials(): List<SvrCredentials> = withContext(Dispatchers.IO) {
@@ -1,5 +1,6 @@
package org.signal.registration.screens.countrycode
import com.google.i18n.phonenumbers.PhoneNumberUtil
import java.util.Locale
/**
@@ -7,6 +8,46 @@ import java.util.Locale
*/
object CountryUtils {
/**
* Representative country for the most popular languages, used to guess a region when a locale carries no explicit
* country (e.g. a bare "en" or "de"). Intentionally not exhaustive; unmapped languages fall through to the caller's
* default.
*/
private val LANGUAGE_TO_REGION: Map<String, String> = mapOf(
"ar" to "SA",
"bn" to "BD",
"cs" to "CZ",
"da" to "DK",
"de" to "DE",
"el" to "GR",
"en" to "US",
"es" to "ES",
"fa" to "IR",
"fi" to "FI",
"fr" to "FR",
"he" to "IL",
"hi" to "IN",
"hu" to "HU",
"id" to "ID",
"it" to "IT",
"ja" to "JP",
"ko" to "KR",
"nb" to "NO",
"nl" to "NL",
"nn" to "NO",
"no" to "NO",
"pl" to "PL",
"pt" to "BR",
"ro" to "RO",
"ru" to "RU",
"sv" to "SE",
"th" to "TH",
"tr" to "TR",
"uk" to "UA",
"vi" to "VN",
"zh" to "CN"
)
@JvmStatic
fun countryToEmoji(countryCode: String): String {
return if (countryCode.isNotEmpty()) {
@@ -19,4 +60,18 @@ object CountryUtils {
""
}
}
/**
* Derives a best-guess dialing region code from a [locale]. Prefers the locale's explicit country when it maps to a
* real dialing region, then falls back to a representative country for the locale's language. Returns null if neither
* yields a usable region.
*/
fun localeToRegionCode(locale: Locale): String? {
val country = locale.country.uppercase(Locale.US)
if (country.isNotEmpty() && PhoneNumberUtil.getInstance().getCountryCodeForRegion(country) != 0) {
return country
}
return LANGUAGE_TO_REGION[locale.language.lowercase(Locale.US)]
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.registration.screens.countrycode
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import org.junit.Test
import java.util.Locale
class CountryUtilsTest {
@Test
fun `localeToRegionCode prefers explicit country when it is a real dialing region`() {
assertThat(CountryUtils.localeToRegionCode(Locale("en", "GB"))).isEqualTo("GB")
assertThat(CountryUtils.localeToRegionCode(Locale("de", "DE"))).isEqualTo("DE")
assertThat(CountryUtils.localeToRegionCode(Locale("pt", "BR"))).isEqualTo("BR")
}
@Test
fun `localeToRegionCode falls back to language mapping when country is absent`() {
assertThat(CountryUtils.localeToRegionCode(Locale("en"))).isEqualTo("US")
assertThat(CountryUtils.localeToRegionCode(Locale("de"))).isEqualTo("DE")
assertThat(CountryUtils.localeToRegionCode(Locale("fr"))).isEqualTo("FR")
assertThat(CountryUtils.localeToRegionCode(Locale("pt"))).isEqualTo("BR")
}
@Test
fun `localeToRegionCode falls back to language mapping when country is not a dialing region`() {
assertThat(CountryUtils.localeToRegionCode(Locale("en", "ZZ"))).isEqualTo("US")
}
@Test
fun `localeToRegionCode returns null when neither country nor language yields a region`() {
assertThat(CountryUtils.localeToRegionCode(Locale("xx"))).isNull()
}
}