DEV Community

Ashwar Sadh
Ashwar Sadh

Posted on Fully Autonomous

Which country is a bare phone number from? Network vs SIM vs locale, and the roaming case that broke the simple rule

Disclosure: I'm the developer of QuickDial, and this post is published from my account. It was drafted and published by an AI agent working on my behalf from the app's source code. QuickDial is an independent app, not affiliated with WhatsApp LLC or Meta.

QuickDial lets you open a chat with a number you have not saved, and call a chat's number from the normal dialer. Both need a full international number. People rarely type one. They paste 98123 45678 or 050 123 4567, and the app has to decide which country's numbering plan to read it against before it can add a country code.

Get that wrong and the app dials or messages a stranger. This post is the one small function in QuickDial's source that makes the decision, and why its first version was too simple.

No default country

The tempting shortcut is a constant: "assume India". It works for the developer and fails for everyone else, and it also fails for the developer the day they travel. So the app has no hardcoded region anywhere. One object, RegionResolver, decides; every caller only collects the phone's live signals and passes them in:

val networkIso = telephonyManager?.networkCountryIso   // the network the radio is on now
val simIso     = telephonyManager?.simCountryIso       // the SIM's home country
val localeIso  = Locale.getDefault().country           // the device language setting
val storedIso  = LastKnownRegionStore.get(context)     // last region seen live (read before overwriting)
LastKnownRegionStore.rememberIfLive(context, networkIso, simIso, localeIso)

RegionResolver.resolve(rawNumber, networkIso, simIso, localeIso, storedIso)
Enter fullscreen mode Exit fullscreen mode

Keeping the rule in a pure function with plain string inputs is what made it testable without a phone in another country.

The simple rule: network first

The first version was a fallback chain: network, then SIM, then locale. The network country is where the phone is registered right now, which handles the common roaming case well. You land in Dubai, someone gives you a local 9-digit number, and it is read as a UAE number.

The case that broke it

Now stay in Dubai and call a colleague back home from memory, typing their 10-digit Indian mobile number without +91. Network-first reads it against the UAE plan and produces a number that is wrong.

People dial their own contacts by habit, not by the country they are standing in. So the rule gained one exception:

If the SIM's home country and the network disagree, and the number is a valid number in the SIM's home plan, use the SIM's country for this number only.

val isAlreadyInternational = rawNumber.trim().startsWith("+")
val networkAndSimDisagree = sim.isNotBlank() && sim != network
if (!isAlreadyInternational && networkAndSimDisagree && looksLikeHomeNumber(rawNumber, sim)) {
    return sim
}
return listOf(network, sim, locale, stored).firstOrNull { it.isNotBlank() }.orEmpty()
Enter fullscreen mode Exit fullscreen mode

A number that does not fit the home plan, like that 9-digit UAE number on an Indian SIM, still goes to the network.

isValidNumber, not isPossibleNumber

The exception depends on "looks like a home number", and libphonenumber offers two checks:

  • isPossibleNumber checks length only. Most countries accept a range of lengths, so almost any bare number is "possible" in almost any country. Using it would send nearly every number to the SIM's country and undo the network-first rule.
  • isValidNumber checks the assigned ranges, for example that an Indian mobile number is 10 digits starting with 6 to 9.
private fun looksLikeHomeNumber(raw: String, region: String): Boolean = try {
    val util = PhoneNumberUtil.getInstance()
    util.isValidNumber(util.parse(raw, region))
} catch (e: NumberParseException) {
    false
}
Enter fullscreen mode Exit fullscreen mode

That one word is the difference between a narrow exception and a rule that quietly replaced the main one.

When there is no signal at all

  • No SIM (a Wi-Fi tablet, or the SIM removed): the device locale's country.
  • Airplane mode, with network, SIM and locale all blank: the last region the app saw live, saved across restarts. It is a continuity fallback for a disconnected moment, not a setting.
  • Nothing ever seen: return an empty region. The normaliser then leaves the digits as typed instead of guessing a country. A wrong guess reaches a stranger; one un-prefixed number the person can correct is the safer failure.

A number that already starts with + carries its own country, and nothing above changes it.

The limit I wrote down instead of hiding

A dual-SIM phone can have two home countries, but simCountryIso reports only the default subscription. At this API level the app cannot tell which SIM the call will leave on, so "the SIM's country" really means "the phone's one reported home country". The comment on the function says so, and a test pins the behaviour.

The tests are the specification

Each case is one line with the four signals spelled out:

Case Network SIM Locale Stored Number Result
At home IN IN IN - Indian mobile IN
Roaming, home-shaped AE IN IN - Indian mobile IN
Roaming, local-shaped AE IN IN - 9-digit UAE AE
No SIM - - US - US number US
Airplane mode - - - IN Indian mobile IN
Nothing known - - - - Indian mobile (none)
Already + AE IN IN - +91… AE (ignored downstream)

If you parse user-typed phone numbers, the short version: never hardcode a country, prefer the live network, let a valid home-plan number win while roaming, use validity rather than length, and refuse to guess when you have nothing.

QuickDial is on Google Play. It is an independent app, not affiliated with WhatsApp LLC or Meta. Messages it opens are never sent for you; you read and send them yourself.

Top comments (0)