Skip to content

Commit c5200af

Browse files
committed
feat(checker): regulator-defined telemarketing prefixes and India 1600 allow
Four opt-in regulatory prefix rules: - Spain +34 400 (RD 2026, commercial call prefix from October 2026) - India +91 140 (TRAI promotional call prefix) - Brazil +55 0303 (ANATEL telemarketing prefix) - India +91 1600 allow floor (banks, insurers, government per TRAI) The three blocking rules run at priority 5310 (between downloaded prefix data and STIR/SHAKEN trusted allow). The India 1600 allow runs at 5250, above the database and heuristic layers. All four are off by default. 12 new tests cover enabled/disabled/wrong-number for each rule plus priority ordering assertions. Test count moves from 1313 to 1325. Tests: new file RegulatoryCheckerTest.kt covers all four rules with positive, negative, and ordering checks. No existing tests changed.
1 parent cde9bea commit c5200af

6 files changed

Lines changed: 195 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ All detection layers implement a shared `IChecker` interface and run in priority
9090
| 5400 | **Range Patterns** (A5) | Block | Length-locked `#` patterns like `+33162######`, with schedule + coverage safety rail |
9191
| 5350 | **Temporary Allow** | Allow | One-off false-positive recovery from the Blocked Log. Beats all downloaded data, never your own rules |
9292
| 5320 | **Prefix Rules** | Block | Downloaded wangiri country codes, US premium rate (+1900), international premium |
93+
| 5310 | **Regulatory Prefix** | Block | Opt-in country telemarketing blocks: Spain 400 (RD 2026), India 140 (TRAI), Brazil 0303 (ANATEL) |
9394
| 5300 | **STIR/SHAKEN Authenticated** | Allow | Carrier-authenticated caller ID allows through heuristic/ML suspicion, and through a database match only when that row's newest evidence, community reports included, is over a year old and the number isn't trending right now. Explicit blocks still win first |
95+
| 5250 | **Regulatory Allow** | Allow | Protected series that must not be blocked. India 1600 (banks, insurers, government per TRAI) |
9496
| 5200 | **Spam Database** | Block | 51,634 imported spam numbers plus the trending-numbers feed |
9597
| 5150 | **Database Prefix Expansion** | Block | Auto-blocks last-two-digit siblings of confirmed database entries |
9698
| 5000 | **Recently Dialed** | Allow | Numbers you called in the last 24h. They're probably calling back |

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@ class SpamRepository(
201201
val KEY_EMERGENCY_CALLBACK_GRACE = booleanPreferencesKey("emergency_callback_grace_enabled")
202202
val KEY_EMERGENCY_CALLBACK_WINDOW_MINUTES = intPreferencesKey("emergency_callback_grace_window_minutes")
203203

204+
val KEY_REG_SPAIN_400 = booleanPreferencesKey("reg_spain_400_enabled")
205+
val KEY_REG_INDIA_140 = booleanPreferencesKey("reg_india_140_enabled")
206+
val KEY_REG_BRAZIL_0303 = booleanPreferencesKey("reg_brazil_0303_enabled")
207+
val KEY_REG_INDIA_1600_ALLOW = booleanPreferencesKey("reg_india_1600_allow_enabled")
208+
204209
// Feature 9: Time-based blocking
205210
val KEY_TIME_BLOCK = booleanPreferencesKey("time_block_enabled")
206211
val KEY_TIME_BLOCK_START = intPreferencesKey("time_block_start_hour") // 0-23

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,36 @@ internal class PrefixChecker(
449449
}
450450
}
451451

452+
internal class RegulatoryPrefixChecker : IChecker {
453+
override val priority = CheckerPriority.REGULATORY_PREFIX
454+
override val name = "regulatory_prefix"
455+
456+
override suspend fun check(ctx: CheckContext): BlockResult? {
457+
if (ctx.prefs[SpamRepository.KEY_REG_SPAIN_400] == true && ctx.number.startsWith("+34400")) {
458+
return BlockResult.block("regulatory_prefix", "telemarketing", "Spain 400 telemarketing (RD 2026)")
459+
}
460+
if (ctx.prefs[SpamRepository.KEY_REG_INDIA_140] == true && ctx.number.startsWith("+91140")) {
461+
return BlockResult.block("regulatory_prefix", "telemarketing", "India 140 promotional (TRAI)")
462+
}
463+
if (ctx.prefs[SpamRepository.KEY_REG_BRAZIL_0303] == true && ctx.number.startsWith("+550303")) {
464+
return BlockResult.block("regulatory_prefix", "telemarketing", "Brazil 0303 telemarketing (ANATEL)")
465+
}
466+
return null
467+
}
468+
}
469+
470+
internal class RegulatoryAllowChecker : IChecker {
471+
override val priority = CheckerPriority.REGULATORY_ALLOW
472+
override val name = "regulatory_allow"
473+
474+
override suspend fun check(ctx: CheckContext): BlockResult? {
475+
if (ctx.prefs[SpamRepository.KEY_REG_INDIA_1600_ALLOW] == true && ctx.number.startsWith("+911600")) {
476+
return BlockResult.allow("india_1600_protected")
477+
}
478+
return null
479+
}
480+
}
481+
452482
/**
453483
* Wildcard / regex rules (Feature 8). Cached list; invalidated on edit.
454484
* A7 schedule gating: rules may carry a day/hour window that skips the

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,9 @@ object CheckerPriority {
246246
// Paired with STIR_SHAKEN (block side) above.
247247
const val TEMPORARY_ALLOW = 5_350 // one-off false-positive recovery
248248
const val PREFIX_MATCH = 5_320 // downloaded prefix reputation rows (country/NPA ranges)
249+
const val REGULATORY_PREFIX = 5_310 // opt-in country telemarketing prefix blocks
249250
const val STIR_SHAKEN_TRUSTED = 5_300
251+
const val REGULATORY_ALLOW = 5_250 // protected series (India 1600)
250252
const val GITHUB_DATABASE = 5_200 // downloaded exact reputation rows
251253
const val DB_PREFIX_EXPANSION = 5_150 // auto-block last-2-digit siblings of DB entries
252254
const val RECENTLY_DIALED = 5_000 // user just called this number
@@ -452,6 +454,8 @@ object SpamCheckers {
452454
add(DbPrefixExpansionChecker(repo))
453455
add(SystemBlockListChecker(appContext))
454456
add(PrefixChecker(repo))
457+
add(RegulatoryPrefixChecker())
458+
add(RegulatoryAllowChecker())
455459
add(WildcardChecker(repo))
456460
add(HashWildcardChecker(repo, dependencies.hashWildcardMatcher))
457461
add(RecentlyDialedChecker(appContext, dependencies.callbackDetector))

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,14 @@
16101610
</plurals>
16111611
<string name="settings_region_cnap_rules">Region &amp; caller-name rules</string>
16121612
<string name="settings_region_cnap_summary">Allowed regions: %1$d · Trusted names: %2$d · Blocked names: %3$d</string>
1613+
<string name="reg_prefix_spain_400">Block Spain 400 telemarketing</string>
1614+
<string name="reg_prefix_spain_400_desc">Spain requires commercial calls to use the 400 prefix from October 2026 (RD 2026).</string>
1615+
<string name="reg_prefix_india_140">Block India 140 promotional</string>
1616+
<string name="reg_prefix_india_140_desc">India reserves the 140 prefix for promotional and telemarketing calls (TRAI).</string>
1617+
<string name="reg_prefix_brazil_0303">Block Brazil 0303 telemarketing</string>
1618+
<string name="reg_prefix_brazil_0303_desc">Brazil\'s 0303 prefix is used for telemarketing calls (ANATEL).</string>
1619+
<string name="reg_prefix_india_1600_allow">Protect India 1600 series</string>
1620+
<string name="reg_prefix_india_1600_allow_desc">India\'s 1600 series is used by banks, insurers, and government. TRAI prohibits blocking it.</string>
16131621
<string name="region_rules_title">Region &amp; caller-name rules</string>
16141622
<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>
16151623
<string name="region_rules_block_outside">Block calls outside allowed regions</string>
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.sysadmindoc.callshield.data.checker
2+
3+
import android.content.Context
4+
import androidx.datastore.preferences.core.emptyPreferences
5+
import androidx.datastore.preferences.core.preferencesOf
6+
import androidx.test.core.app.ApplicationProvider
7+
import com.sysadmindoc.callshield.data.SpamRepository
8+
import kotlinx.coroutines.runBlocking
9+
import org.junit.Assert.assertEquals
10+
import org.junit.Assert.assertNotNull
11+
import org.junit.Assert.assertNull
12+
import org.junit.Assert.assertTrue
13+
import org.junit.Test
14+
import org.junit.runner.RunWith
15+
import org.robolectric.RobolectricTestRunner
16+
import org.robolectric.annotation.Config
17+
18+
@RunWith(RobolectricTestRunner::class)
19+
@Config(sdk = [34])
20+
class RegulatoryCheckerTest {
21+
private val context: Context = ApplicationProvider.getApplicationContext()
22+
23+
private fun ctx(
24+
number: String,
25+
prefs: androidx.datastore.preferences.core.Preferences = emptyPreferences(),
26+
) = CheckContext(appContext = context, number = number, realtimeCall = true, prefs = prefs)
27+
28+
@Test
29+
fun `Spain 400 blocks when enabled`() =
30+
runBlocking {
31+
val result =
32+
RegulatoryPrefixChecker().check(
33+
ctx("+34400123456", preferencesOf(SpamRepository.KEY_REG_SPAIN_400 to true)),
34+
)
35+
assertNotNull(result)
36+
assertEquals("regulatory_prefix", result!!.matchSource)
37+
}
38+
39+
@Test
40+
fun `Spain 400 passes when disabled`() =
41+
runBlocking {
42+
assertNull(
43+
RegulatoryPrefixChecker().check(
44+
ctx("+34400123456", preferencesOf(SpamRepository.KEY_REG_SPAIN_400 to false)),
45+
),
46+
)
47+
}
48+
49+
@Test
50+
fun `Spain non-400 number passes`() =
51+
runBlocking {
52+
assertNull(
53+
RegulatoryPrefixChecker().check(
54+
ctx("+34612345678", preferencesOf(SpamRepository.KEY_REG_SPAIN_400 to true)),
55+
),
56+
)
57+
}
58+
59+
@Test
60+
fun `India 140 blocks when enabled`() =
61+
runBlocking {
62+
val result =
63+
RegulatoryPrefixChecker().check(
64+
ctx("+911401234567", preferencesOf(SpamRepository.KEY_REG_INDIA_140 to true)),
65+
)
66+
assertNotNull(result)
67+
assertEquals("regulatory_prefix", result!!.matchSource)
68+
}
69+
70+
@Test
71+
fun `India 140 passes when disabled`() =
72+
runBlocking {
73+
assertNull(
74+
RegulatoryPrefixChecker().check(
75+
ctx("+911401234567", preferencesOf(SpamRepository.KEY_REG_INDIA_140 to false)),
76+
),
77+
)
78+
}
79+
80+
@Test
81+
fun `Brazil 0303 blocks when enabled`() =
82+
runBlocking {
83+
val result =
84+
RegulatoryPrefixChecker().check(
85+
ctx("+5503031234567", preferencesOf(SpamRepository.KEY_REG_BRAZIL_0303 to true)),
86+
)
87+
assertNotNull(result)
88+
assertEquals("regulatory_prefix", result!!.matchSource)
89+
}
90+
91+
@Test
92+
fun `Brazil 0303 passes when disabled`() =
93+
runBlocking {
94+
assertNull(
95+
RegulatoryPrefixChecker().check(
96+
ctx("+5503031234567", preferencesOf(SpamRepository.KEY_REG_BRAZIL_0303 to false)),
97+
),
98+
)
99+
}
100+
101+
@Test
102+
fun `India 1600 allows when enabled`() =
103+
runBlocking {
104+
val result =
105+
RegulatoryAllowChecker().check(
106+
ctx("+9116001234567", preferencesOf(SpamRepository.KEY_REG_INDIA_1600_ALLOW to true)),
107+
)
108+
assertNotNull(result)
109+
assertEquals("india_1600_protected", result!!.matchSource)
110+
assertTrue(!result.shouldBlock)
111+
}
112+
113+
@Test
114+
fun `India 1600 passes when disabled`() =
115+
runBlocking {
116+
assertNull(
117+
RegulatoryAllowChecker().check(
118+
ctx("+9116001234567", preferencesOf(SpamRepository.KEY_REG_INDIA_1600_ALLOW to false)),
119+
),
120+
)
121+
}
122+
123+
@Test
124+
fun `India non-1600 number is not allowed`() =
125+
runBlocking {
126+
assertNull(
127+
RegulatoryAllowChecker().check(
128+
ctx("+911234567890", preferencesOf(SpamRepository.KEY_REG_INDIA_1600_ALLOW to true)),
129+
),
130+
)
131+
}
132+
133+
@Test
134+
fun `regulatory allow sits above database and heuristic blocks`() {
135+
val allow = RegulatoryAllowChecker()
136+
assertTrue(allow.priority > CheckerPriority.GITHUB_DATABASE)
137+
assertTrue(allow.priority > CheckerPriority.HEURISTIC)
138+
}
139+
140+
@Test
141+
fun `regulatory prefix sits below the downloaded prefix matcher`() {
142+
val prefix = RegulatoryPrefixChecker()
143+
assertTrue(prefix.priority < CheckerPriority.PREFIX_MATCH)
144+
assertTrue(prefix.priority > CheckerPriority.STIR_SHAKEN_TRUSTED)
145+
}
146+
}

0 commit comments

Comments
 (0)