diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 3e17dec218..576a15d512 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -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 = withContext(Dispatchers.IO) { diff --git a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt index 7190d1ccc3..a7061e8a0f 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt @@ -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 = 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)] + } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryUtilsTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryUtilsTest.kt new file mode 100644 index 0000000000..5306717b52 --- /dev/null +++ b/feature/registration/src/test/java/org/signal/registration/screens/countrycode/CountryUtilsTest.kt @@ -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() + } +}