Skip to content

Commit 4bfc47b

Browse files
committed
fix(region): allow countries by calling code so two-letter codes stay NANP
5a0830d put ISO country codes in the same allowlist as US state and Canadian province codes. CO, IN, PA, AL, AR, DE, ID, IL, MA and NL exist in both, and isOutsideAllowedRegions treated an allowed state as the country as well, so a Colorado user allowing CO also let every Colombian number ring and an Indiana user allowed all of India. Countries are now entered and stored as calling codes (+57, +39). A code must be a whole assigned ITU-T code (prefix-free check shared with the report normalizer, moved to util/PhoneDigits), so +3, +35 and +391 are rejected. +1 plus an area code (+1809) names the Caribbean countries that share +1, and "+1 809" typed with a space is read as that area code rather than all of +1. Two-letter codes keep their NANP meaning, so stored values are unchanged; the ISO form never shipped in a release. regionCode() also stops reading a ten-digit international number as NANP: Penang's +60 4 counted as Vancouver 604 and Hamilton NZ's +64 7 as Toronto 647, so allowing BC or ON let them ring (shipped in v1.7.38). Tests: the five country cases in RegionRulesTest asserted the colliding ISO behavior (CO allowing +57); they now use calling codes, and new cases pin CO, IN and PA both ways, code validation, the spaced area code and the ten-digit misread. The NANP guard was mutation-checked. UI hint, disclosure and README describe the calling-code form; zh-rCN strings updated to match.
1 parent 8bdc8be commit 4bfc47b

8 files changed

Lines changed: 163 additions & 138 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ All notable changes to CallShield will be documented in this file.
9696
that, so a tap on a list's name can't take its numbers out. A list you remove
9797
can be put back with Undo right afterwards, numbers included, without
9898
downloading it again.
99+
- Region rules can allow countries outside North America. Add a country by
100+
its calling code, like +39 for Italy or +57 for Colombia, next to the
101+
two-letter state and province codes. A Caribbean country that shares +1
102+
goes in by area code, so the Dominican Republic is +1809. Two-letter codes
103+
always mean the state or province, and CO is Colorado, never Colombia.
104+
- Region rules no longer mistake a ten-digit number from outside North America
105+
for a North American one. A Penang landline (+60 4) counted as Vancouver's
106+
604 area code, so allowing British Columbia let it ring.
99107

100108
### Interface
101109

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ All detection layers implement a shared `IChecker` interface and run in priority
102102
| 4850 | **Caller Name Trust** | Allow | Carrier-presented name matches one of your trust patterns (requires the device to provide a name during screening) |
103103
| 4700 | **Push-Alert Bridge** (A3) | Allow | Uber/DoorDash/Amazon/Gmail notification about an arriving call? Let it through |
104104
| 4500 | *Campaign Recorder* |. | Side-effect only; feeds burst detection below |
105-
| 4300 | **Region Rules** | Block | Opt-in offline blocking outside your selected US/Canadian regions |
105+
| 4300 | **Region Rules** | Block | Opt-in offline blocking outside the US/Canadian regions and country calling codes you allow |
106106
| 4000 | **Quiet Hours** | Block | Block all non-contact calls during configurable hours (calls only) |
107107
| 3500 | **Frequency Auto-Block** | Block | Numbers that call 3+ times in 7 days get auto-blocked |
108108
| 3000 | **Heuristic Engine** | Block | VoIP ranges, neighbor spoofing, rapid-fire detection, 30+ rules |

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

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.sysadmindoc.callshield.data
33
import android.content.Context
44
import com.sysadmindoc.callshield.data.remote.HttpClient
55
import com.sysadmindoc.callshield.service.CommunityReportWorker
6+
import com.sysadmindoc.callshield.util.countryCallingCodeOf
67
import kotlinx.coroutines.CancellationException
78
import kotlinx.coroutines.Dispatchers
89
import kotlinx.coroutines.withContext
@@ -310,35 +311,13 @@ object CommunityContributor {
310311
* in step so app reports and issue-filed reports land on the same key.
311312
*/
312313
internal fun stripNationalTrunkPrefix(digits: String): String {
313-
val countryCode =
314-
when {
315-
digits.isEmpty() -> return digits
316-
digits.take(1) in ONE_DIGIT_COUNTRY_CODES -> digits.take(1)
317-
digits.take(2) in TWO_DIGIT_COUNTRY_CODES -> digits.take(2)
318-
digits.length >= 3 -> digits.take(3)
319-
else -> return digits
320-
}
314+
val countryCode = countryCallingCodeOf(digits) ?: return digits
321315
if (countryCode in TRUNK_ZERO_SIGNIFICANT_COUNTRY_CODES) return digits
322316
val national = digits.drop(countryCode.length).trimStart('0')
323317
// An all-zero national part is junk; leave it for the length check to reject.
324318
return if (national.isEmpty()) digits else countryCode + national
325319
}
326320

327-
/** Every assigned one/two-digit ITU-T E.164 country calling code. Calling codes are
328-
* prefix-free, so anything not matching these is a three-digit code. */
329-
private val ONE_DIGIT_COUNTRY_CODES = setOf("1", "7")
330-
331-
private val TWO_DIGIT_COUNTRY_CODES =
332-
(
333-
"20 27 " +
334-
"30 31 32 33 34 36 39 " +
335-
"40 41 43 44 45 46 47 48 49 " +
336-
"51 52 53 54 55 56 57 58 " +
337-
"60 61 62 63 64 65 66 " +
338-
"81 82 84 86 " +
339-
"90 91 92 93 94 95 98"
340-
).split(" ").toSet()
341-
342321
/** Countries whose national significant numbers genuinely keep a leading 0 in
343322
* E.164 — stripping it there would corrupt the number, not repair it:
344323
* +39 Italy (and Vatican City, which splits under 39): +39 06 … is correct

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

Lines changed: 44 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package com.sysadmindoc.callshield.data
22

33
import com.sysadmindoc.callshield.data.areacodes.AreaCodeLookup
4+
import com.sysadmindoc.callshield.util.countryCallingCodeOf
5+
import com.sysadmindoc.callshield.util.filterAsciiDigits
6+
import com.sysadmindoc.callshield.util.isAsciiDigit
47
import java.util.Locale
58

69
/** Pure, bounded matching helpers for the regional and carrier-name rules. */
710
object RegionRules {
811
const val MAX_ALLOWED_REGIONS = 64
912
const val MAX_NAME_PATTERNS = 30
1013
const val MAX_NAME_PATTERN_LENGTH = 60
14+
private const val NANP_NATIONAL_LENGTH = 10
15+
16+
/** `1` plus a three-digit area code, as in `+1809`. */
17+
private const val NANP_AREA_CODE_ENTRY_LENGTH = 4
1118

1219
val supportedRegionCodes: Set<String> =
1320
setOf(
@@ -76,104 +83,33 @@ object RegionRules {
7683
"TF",
7784
)
7885

79-
private val countryCallingCodes: Map<String, String> =
80-
mapOf(
81-
"AF" to "+93",
82-
"AL" to "+355",
83-
"DZ" to "+213",
84-
"AR" to "+54",
85-
"AU" to "+61",
86-
"AT" to "+43",
87-
"BD" to "+880",
88-
"BE" to "+32",
89-
"BR" to "+55",
90-
"BG" to "+359",
91-
"KH" to "+855",
92-
"CM" to "+237",
93-
"CL" to "+56",
94-
"CN" to "+86",
95-
"CO" to "+57",
96-
"HR" to "+385",
97-
"CZ" to "+420",
98-
"DK" to "+45",
99-
"EG" to "+20",
100-
"FI" to "+358",
101-
"FR" to "+33",
102-
"DE" to "+49",
103-
"GH" to "+233",
104-
"GR" to "+30",
105-
"HK" to "+852",
106-
"HU" to "+36",
107-
"IN" to "+91",
108-
"ID" to "+62",
109-
"IR" to "+98",
110-
"IQ" to "+964",
111-
"IE" to "+353",
112-
"IL" to "+972",
113-
"IT" to "+39",
114-
"JP" to "+81",
115-
"JO" to "+962",
116-
"KE" to "+254",
117-
"KR" to "+82",
118-
"KW" to "+965",
119-
"MY" to "+60",
120-
"MX" to "+52",
121-
"MA" to "+212",
122-
"NL" to "+31",
123-
"NZ" to "+64",
124-
"NG" to "+234",
125-
"NO" to "+47",
126-
"PK" to "+92",
127-
"PE" to "+51",
128-
"PH" to "+63",
129-
"PL" to "+48",
130-
"PT" to "+351",
131-
"RO" to "+40",
132-
"RU" to "+7",
133-
"SA" to "+966",
134-
"SG" to "+65",
135-
"ZA" to "+27",
136-
"ES" to "+34",
137-
"SE" to "+46",
138-
"CH" to "+41",
139-
"TW" to "+886",
140-
"TH" to "+66",
141-
"TR" to "+90",
142-
"UA" to "+380",
143-
"AE" to "+971",
144-
"GB" to "+44",
145-
"VN" to "+84",
146-
"EC" to "+593",
147-
"VE" to "+58",
148-
"UY" to "+598",
149-
"PY" to "+595",
150-
"BO" to "+591",
151-
"PA" to "+507",
152-
"CR" to "+506",
153-
"GT" to "+502",
154-
"HN" to "+504",
155-
"SV" to "+503",
156-
"NI" to "+505",
157-
"DO" to "+1809",
158-
"JM" to "+1876",
159-
"TT" to "+1868",
160-
)
161-
162-
fun countryCallingCodeFor(isoCode: String): String? = countryCallingCodes[isoCode.uppercase(Locale.ROOT)]
163-
164-
val supportedCountryCodes: Set<String> = countryCallingCodes.keys
165-
166-
fun parseRegionCodes(raw: String): Set<String> = normalizeRegionCodes(raw.split(',', ';', '\n', '\t', ' '))
86+
// "+1 809" names one area code. Split on the space, it would allow all of +1.
87+
fun parseRegionCodes(raw: String): Set<String> = normalizeRegionCodes(raw.replace(SPACED_NANP_AREA_CODE, "+1").split(',', ';', '\n', '\t', ' '))
16788

16889
fun normalizeRegionCodes(regions: Iterable<String>): Set<String> =
16990
regions
17091
.asSequence()
17192
.map { it.trim().uppercase(Locale.ROOT) }
172-
.filter { it in supportedRegionCodes || it in supportedCountryCodes }
93+
.mapNotNull { code -> code.takeIf { it in supportedRegionCodes } ?: normalizeDialingCode(code) }
17394
.distinct()
17495
.take(MAX_ALLOWED_REGIONS)
17596
.toCollection(linkedSetOf())
17697

98+
/**
99+
* A country is allowed by its calling code (`+57`), never by a two-letter
100+
* ISO code: CO, IN, PA, SK and many more ISO codes are also US state or
101+
* Canadian province codes, and a two-letter entry keeps meaning the state
102+
* or province. An area code (`+1809`) names one of the Caribbean countries
103+
* that share +1. Returns [code] when it is one of those forms.
104+
*/
105+
fun normalizeDialingCode(code: String): String? {
106+
if (!code.startsWith("+")) return null
107+
val digits = code.substring(1)
108+
if (digits.isEmpty() || digits[0] == '0' || !digits.all { it.isAsciiDigit() }) return null
109+
val isNanpAreaCode = digits.length == NANP_AREA_CODE_ENTRY_LENGTH && digits[0] == '1' && digits[1] in '2'..'9'
110+
return code.takeIf { isNanpAreaCode || countryCallingCodeOf(digits) == digits }
111+
}
112+
177113
fun parseNamePatterns(raw: String): Set<String> = normalizeNamePatterns(raw.split('\n', ',', ';'))
178114

179115
fun normalizeNamePatterns(patterns: Iterable<String>): Set<String> =
@@ -186,7 +122,12 @@ object RegionRules {
186122
.take(MAX_NAME_PATTERNS)
187123
.toCollection(linkedSetOf())
188124

189-
fun regionCode(number: String): String? = AreaCodeLookup.getRegionCode(number)
125+
/**
126+
* The NANP state, province or territory code of [number], or null. A `+`
127+
* number outside +1 is never NANP, even when it has ten digits: Penang's
128+
* +60 4 would otherwise read as Vancouver's 604.
129+
*/
130+
fun regionCode(number: String): String? = if (number.startsWith("+") && !number.startsWith("+1")) null else AreaCodeLookup.getRegionCode(number)
190131

191132
fun isOutsideAllowedRegions(
192133
number: String,
@@ -196,11 +137,19 @@ object RegionRules {
196137
if (normalized.isEmpty()) return false
197138
val nanpRegion = regionCode(number)
198139
if (nanpRegion != null && nanpRegion in normalized) return false
199-
for (code in normalized) {
200-
val prefix = countryCallingCodeFor(code) ?: continue
201-
if (number.startsWith(prefix)) return false
140+
val international = internationalForm(number) ?: return true
141+
return normalized.none { it.startsWith("+") && international.startsWith(it) }
142+
}
143+
144+
/** [number] as `+<digits>`, reading a bare ten- or eleven-digit number as NANP. */
145+
private fun internationalForm(number: String): String? {
146+
if (number.startsWith("+")) return number
147+
val digits = filterAsciiDigits(number)
148+
return when {
149+
digits.length == NANP_NATIONAL_LENGTH -> "+1$digits"
150+
digits.length == NANP_NATIONAL_LENGTH + 1 && digits.startsWith("1") -> "+$digits"
151+
else -> null
202152
}
203-
return true
204153
}
205154

206155
fun matchesPresentedName(
@@ -257,4 +206,5 @@ object RegionRules {
257206
}
258207

259208
private val WHITESPACE = Regex("\\s+")
209+
private val SPACED_NANP_AREA_CODE = Regex("""\+1[ \t]+(?=[2-9][0-9]{2}(?![0-9]))""")
260210
}

‎app/src/main/java/com/sysadmindoc/callshield/util/PhoneDigits.kt‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ fun hasMinAsciiDigits(
8989
minimum: Int = MIN_CONFIRMABLE_PHONE_DIGITS,
9090
): Boolean = filterAsciiDigits(value).length >= minimum
9191

92+
/**
93+
* The ITU-T E.164 country calling code that [digits] (no `+`) starts with, or
94+
* null when there are too few digits to hold one. Calling codes are
95+
* prefix-free: 1 and 7 are the only one-digit codes, the two-digit ones are
96+
* listed in [TWO_DIGIT_CALLING_CODES], and every other code has three digits.
97+
*/
98+
fun countryCallingCodeOf(digits: String): String? =
99+
when {
100+
digits.isEmpty() -> null
101+
digits.take(1) in ONE_DIGIT_CALLING_CODES -> digits.take(1)
102+
digits.take(2) in TWO_DIGIT_CALLING_CODES -> digits.take(2)
103+
digits.length >= 3 -> digits.take(3)
104+
else -> null
105+
}
106+
92107
private fun Char.isPhoneFormatControl(): Boolean =
93108
when (code) {
94109
ZERO_WIDTH_SPACE,
@@ -102,6 +117,20 @@ private fun Char.isPhoneFormatControl(): Boolean =
102117
else -> false
103118
}
104119

120+
/** Every assigned one- and two-digit ITU-T E.164 country calling code. */
121+
private val ONE_DIGIT_CALLING_CODES = setOf("1", "7")
122+
123+
private val TWO_DIGIT_CALLING_CODES =
124+
(
125+
"20 27 " +
126+
"30 31 32 33 34 36 39 " +
127+
"40 41 43 44 45 46 47 48 49 " +
128+
"51 52 53 54 55 56 57 58 " +
129+
"60 61 62 63 64 65 66 " +
130+
"81 82 84 86 " +
131+
"90 91 92 93 94 95 98"
132+
).split(" ").toSet()
133+
105134
private const val DEFAULT_PHONE_INPUT_LENGTH = 24
106135
private const val MAX_PHONE_INPUT_SCAN_LENGTH = 256
107136
private const val MIN_CONFIRMABLE_PHONE_DIGITS = 5

‎app/src/main/res/values-zh-rCN/strings.xml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,12 +1076,12 @@
10761076
<string name="settings_region_cnap_rules">地区与主叫姓名规则</string>
10771077
<string name="settings_region_cnap_summary">%1$d 个允许地区 · %2$d 个可信姓名 · %3$d 个拦截姓名</string>
10781078
<string name="region_rules_title">地区与主叫姓名规则</string>
1079-
<string name="region_rules_disclosure">这些规则完全在你的设备上运行。地区拦截使用来电号码的 NANP 区号。主叫姓名信任仅使用 Android 暴露给筛查服务的运营商呈现姓名。</string>
1079+
<string name="region_rules_disclosure">这些规则完全在你的设备上运行。地区拦截对北美号码读取区号,对其他号码读取国家代码。主叫姓名信任仅使用 Android 暴露给筛查服务的运营商呈现姓名。</string>
10801080
<string name="region_rules_block_outside">拦截允许地区之外的来电</string>
1081-
<string name="region_rules_block_outside_desc">允许所选美国各州、加拿大各省与领地;拦截其他及国际号码。</string>
1081+
<string name="region_rules_block_outside_desc">只有你列出的地区和国家的来电会响铃,其他来电都会被拦截。</string>
10821082
<string name="region_rules_allowed_regions">允许的地区代码</string>
1083-
<string name="region_rules_regions_hint">逗号分隔的两字母代码,例如 NY, NJ, ON。用 TF 表示免费电话。</string>
1084-
<string name="region_rules_regions_required">请至少添加一个有效的两字母地区代码。</string>
1083+
<string name="region_rules_regions_hint">用逗号分隔。美国州或加拿大省用两个字母(NY、ON),免费电话用 TF。国家用 + 加国际区号,例如意大利 +39,多米尼加共和国 +1809。</string>
1084+
<string name="region_rules_regions_required">请至少添加一个有效的地区或国家代码。</string>
10851085
<string name="cnap_trust_title">信任呈现的主叫姓名</string>
10861086
<string name="cnap_trust_desc">当运营商提供主叫姓名时,匹配的来电可绕过地区与统计拦截。</string>
10871087
<string name="cnap_trust_patterns">可信姓名模式</string>

‎app/src/main/res/values/strings.xml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,15 +1619,15 @@
16191619
<string name="reg_prefix_india_1600_allow">Protect India 1600 series</string>
16201620
<string name="reg_prefix_india_1600_allow_desc">India\'s 1600 series is used by banks, insurers, and government. TRAI prohibits blocking it.</string>
16211621
<string name="region_rules_title">Region &amp; caller-name rules</string>
1622-
<string name="region_rules_disclosure">These rules run entirely on your device. Region blocking uses the incoming number\'s NANP area code. Caller-name trust uses only a carrier-presented name that Android exposes to the screening service.</string>
1622+
<string name="region_rules_disclosure">These rules run entirely on your device. Region blocking reads the area code of a North American number and the country code of any other. Caller-name trust uses only a carrier-presented name that Android exposes to the screening service.</string>
16231623
<string name="region_rules_block_outside">Block calls outside allowed regions</string>
1624-
<string name="region_rules_block_outside_desc">Allow selected US states, Canadian provinces, and territories; block other and international numbers.</string>
1624+
<string name="region_rules_block_outside_desc">Only the regions and countries you list can ring. Calls from anywhere else are blocked.</string>
16251625
<string name="region_rules_allowed_regions">Allowed region codes</string>
1626-
<string name="region_rules_regions_hint">Comma-separated two-letter codes, for example NY, NJ, ON. Use TF for toll-free.</string>
1627-
<string name="region_rules_regions_required">Add at least one valid two-letter region code.</string>
1626+
<string name="region_rules_regions_hint">Separate with commas. Use two letters for a US state or Canadian province (NY, ON) and TF for toll-free. Use + and the calling code for a country, like +39 for Italy or +1809 for the Dominican Republic.</string>
1627+
<string name="region_rules_regions_required">Add at least one valid region or country code.</string>
16281628
<plurals name="region_rules_regions_count">
1629-
<item quantity="one">%1$d valid region code</item>
1630-
<item quantity="other">%1$d valid region codes</item>
1629+
<item quantity="one">%1$d valid code</item>
1630+
<item quantity="other">%1$d valid codes</item>
16311631
</plurals>
16321632
<string name="cnap_device_unavailable_notice">This device hasn\'t provided a caller name during screening. These rules won\'t take effect until it does.</string>
16331633
<string name="cnap_trust_title">Trust a presented caller name</string>

0 commit comments

Comments
 (0)