Skip to content

Commit cb0de54

Browse files
committed
fix(region): read domestic numbers that begin like a dialing prefix as domestic
The national-00 table came from scanning libphonenumber's pattern alternatives for a literal leading "00". That missed patterns that reach a stripped prefix through a character class or through the region's own international prefix, so region rules misread and blocked valid domestic callers: a Tajik mobile such as 004104761 read as +4104761 through the ITU 00 fallback, and a Chilean toll-free 12300201234 read as Unreadable (Chile's own prefix 1230, then a 0). The table now holds every national-number pattern (phonenumbers 9.0.39, all number types) whose language intersects "own prefix or 00, then digits", found by intersecting the two as automata. That adds BY (810 premium), CL (1230 toll-free), KZ and RU (8108 toll-free) and TJ (mobiles from 00) to the nine regions it had. Checked outside the build against phonenumbers with a port of readBareNumber: 1,140 sampled national numbers that start with a stripped prefix all read National, 0 problems, while the old table fails the same check 363 times. International strings changed reading only where they are valid national numbers of the home region. Tests: new cases only. The new test fails on the old table.
1 parent 0328f1a commit cb0de54

3 files changed

Lines changed: 55 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,11 @@ All notable changes to CallShield will be documented in this file.
170170
allowing your country's code lets it ring. One that starts with your
171171
country's international prefix (00 in most of Europe, 011 in North America)
172172
counts as the country dialed after it. Domestic numbers that happen to start
173-
with 00, like Bulgaria's 00800 toll-free range, still count as domestic, and
174-
a prefix followed by digits no country code starts with is blocked instead
175-
of passing as a local call.
173+
the same way still count as domestic. That covers Bulgaria's 00800
174+
toll-free range, Tajik mobiles that begin with 00, Chile's 1230 toll-free
175+
numbers, and Belarusian, Russian and Kazakh ranges that begin with 810. A
176+
prefix followed by digits no country code starts with is blocked instead of
177+
passing as a local call.
176178
- Settings has a Telemarketing ranges card for number ranges regulators set
177179
aside for sales calls. It can block Spain's 400 range (required for
178180
commercial calls from 17 October 2026), India's 140 promotional series and

‎app/src/main/java/com/sysadmindoc/callshield/data/RegionCallingCodes.kt‎

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ internal object RegionCallingCodes {
5757

5858
/**
5959
* Reads bare [digits] the way libphonenumber would on a phone from
60-
* [regionIso]. A few regions have domestic numbers that start with 00
61-
* (toll-free 00800 in Bulgaria, for one), and those stay national. Then
60+
* [regionIso]. A few regions have domestic numbers that start with 00 or
61+
* with their own international prefix (toll-free 00800 in Bulgaria, Tajik
62+
* mobiles from 00, toll-free 1230 in Chile), and those stay national. Then
6263
* the region's own international prefix (`011`, `810`, `010`...), or the
6364
* ITU `00` that most networks also accept, marks an international number.
6465
* A calling code never starts with 0, so a prefix followed by 0 can't be
@@ -70,7 +71,7 @@ internal object RegionCallingCodes {
7071
regionIso: String?,
7172
): BareNumber {
7273
val region = regionIso?.uppercase(Locale.ROOT)
73-
val nationalWithPrefix = region?.let { nationalNumbersStartingWith00[it] }
74+
val nationalWithPrefix = region?.let { nationalNumbersStartingWithPrefix[it] }
7475
if (nationalWithPrefix != null && regex("^(?:$nationalWithPrefix)$").matches(digits)) return BareNumber.National
7576
val code = region?.let { codes[it] }
7677
val ownPrefix = region?.let { internationalPrefixOverrides[it] } ?: if (code == null || code == NANP) NANP_PREFIX else ITU_PREFIX
@@ -92,25 +93,38 @@ internal object RegionCallingCodes {
9293
private val compiled = ConcurrentHashMap<String, Regex>()
9394

9495
/**
95-
* National numbers that start with 00, as the alternatives of
96-
* libphonenumber's national-number patterns (every number type) that begin
97-
* with 00, per region (phonenumbers 9.0.39). Android can't put these in
98-
* international form, so they arrive bare, and in BG, PA, QA and TH they
99-
* also start with the international prefix.
96+
* National numbers that start with a prefix the reader strips: the
97+
* region's own international prefix or the ITU 00. These are the
98+
* libphonenumber national-number patterns (phonenumbers 9.0.39, every
99+
* number type) whose language intersects "that prefix, then digits",
100+
* found by intersecting the two as automata rather than by looking for a
101+
* literal leading 00, which missed Tajik mobiles (`0[0-57-9]...`) and
102+
* Chilean toll-free numbers (`1230...`). Android can't put these in
103+
* international form, so they arrive bare.
100104
*/
101-
private val nationalNumbersStartingWith00: Map<String, String> =
105+
private val nationalNumbersStartingWithPrefix: Map<String, String> =
102106
mapOf(
103107
"BG" to """(?:00800\d\d|800)\d{5}""",
104-
"ID" to """00(?:1803\d{5,11}|7803\d{7})|001803\d{5,11}|(?:007803\d|8071)\d{6}""",
105-
"IN" to """000800\d{7}|(?:000800|18(?:03\d\d|6(?:0|[12]\d\d)))\d{7}""",
108+
"BY" to """(?:810|902)\d{7}|800\d{3,7}|(?:8(?:0[13]|10|20\d)|902)\d{7}""",
109+
"CL" to """(?:123|8)00\d{6}""",
110+
"ID" to
111+
"""00(?:1803\d{5,11}|7803\d{7})|(?:177\d|800)\d{5,7}|001803\d{5,11}|(?:007803\d|8071)\d{6}""",
112+
"IN" to
113+
"""000800\d{7}|180(?:0\d{4,9}|3\d{9})|1800\d{4,9}|(?:000800|18(?:03\d\d|6(?:0|[12]\d\d)))\d{7}""",
106114
"JP" to
107115
"""00777(?:[01]|5\d)\d\d|(?:00(?:7778|882[1245])|(?:120|800\d)\d\d)\d{4}|00(?:37|66|78)\d{6,13}""" +
108-
"""|00(?:777(?:[01]|(?:5|8\d)\d)|882[1245]\d\d)\d\d""",
109-
"KR" to """00(?:308\d{6,7}|798\d{7,9})|(?:00368|[38]0)\d{7}|00(?:3(?:08\d{6,7}|68\d{7})|798\d{7,9})""",
110-
"PA" to """(?:00800|800\d)\d{6}""",
111-
"QA" to """(?:0080[01]|800)\d{6}""",
116+
"""|00(?:777(?:[01]|(?:5|8\d)\d)|882[1245]\d\d)\d\d|00(?:37|66|78)\d{6,13}""",
117+
"KR" to
118+
"""00(?:308\d{6,7}|798\d{7,9})|(?:00368|[38]0)\d{7}|00(?:3(?:08\d{6,7}|68\d{7})|798\d{7,9})""",
119+
"KZ" to """8(?:00|108\d{3})\d{7}""",
120+
"PA" to """800\d{4,5}|(?:00800|800\d)\d{6}""",
121+
"QA" to """800\d{4}|(?:0080[01]|800)\d{6}""",
122+
"RU" to """8(?:0[04]|108\d{3})\d{7}""",
112123
"TH" to """(?:001800\d|1800)\d{6}""",
113-
"UY" to """0004\d{2,9}""",
124+
"TJ" to
125+
"""(?:33[03-9]|4(?:1[18]|4[02-479])|81[1-9])\d{6}""" +
126+
"""|(?:[09]\d|1[0-27-9]|2[0-27]|3[08]|40|5[05]|66|7[0157-9]|8[07-9])\d{7}""",
127+
"UY" to """0004\d{2,9}|(?:405|80[05])\d{4}""",
114128
)
115129

116130
/**

‎app/src/test/java/com/sysadmindoc/callshield/data/RegionRulesTest.kt‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,26 @@ class RegionRulesTest {
255255
assertFalse(RegionRules.isOutsideAllowedRegions("0018001234567", setOf("+66"), homeRegionIso = "TH"))
256256
}
257257

258+
@Test
259+
fun `domestic numbers that start like an international prefix stay domestic`() {
260+
val national = RegionCallingCodes.BareNumber.National
261+
// Tajik mobiles from 00 (TJ dials out with 810, so 00 is only the ITU fallback).
262+
assertEquals(national, RegionCallingCodes.readBareNumber("004104761", "TJ"))
263+
assertEquals(national, RegionCallingCodes.readBareNumber("000845221", "TJ"))
264+
// Chilean toll-free 1230 0xx xxxx, which begins with Chile's own prefix 1230 and a 0.
265+
assertEquals(national, RegionCallingCodes.readBareNumber("12300201234", "CL"))
266+
// Belarusian premium numbers from 810, Belarus's own prefix.
267+
assertEquals(national, RegionCallingCodes.readBareNumber("8101234567", "BY"))
268+
assertFalse(RegionRules.isOutsideAllowedRegions("004104761", setOf("+992"), homeRegionIso = "TJ"))
269+
assertFalse(RegionRules.isOutsideAllowedRegions("12300201234", setOf("+56"), homeRegionIso = "CL"))
270+
// Dialing out still reads as international in those regions.
271+
val uk = RegionCallingCodes.BareNumber.International("442079460000")
272+
assertEquals(uk, RegionCallingCodes.readBareNumber("00442079460000", "TJ"))
273+
assertEquals(uk, RegionCallingCodes.readBareNumber("810442079460000", "TJ"))
274+
assertEquals(uk, RegionCallingCodes.readBareNumber("1230442079460000", "CL"))
275+
assertEquals(uk, RegionCallingCodes.readBareNumber("810442079460000", "BY"))
276+
}
277+
258278
@Test
259279
fun `a bare number that can't be read never passes as the home country`() {
260280
// "000..." on an Italian phone is neither Italian nor international.

0 commit comments

Comments
 (0)