Releases: SysAdminDoc/CallShield
v1.7.1 \u2014 caller-ID race + push-allow feedback
Caller-ID overlay no longer blocks on all three external lookups (first spam-hit-wins). Push-allow registry now surfaces "Allowed by you" notices for previously-allowed numbers.
CallShield v1.7.0
v1.7.0 — Round-2/3 OSS borrow & harden
Competitor-OSS research (aj3423/SpamBlocker, adamff-dev/spam-call-blocker-app, rspamd patterns) distilled into two user-facing detection-pipeline improvements, one behavior-preserving refactor, and 14 new JVM unit tests.
Added
- STIR/SHAKEN Trusted-Caller Allow — new detection layer that short-circuits heuristic / ML / campaign-burst / frequency blocks when the carrier signs a
PASSEDattestation on the calling number. Priority slotSTIR_SHAKEN_TRUSTED = 5_300sits below every explicit user rule (manual whitelist, contact whitelist, user blocklist, prefix, wildcard, hash-wildcard, system-block-list) so an intentionally blocked number stays blocked even if the carrier verifies it. New settingKEY_STIR_TRUSTED_ALLOWdefaults on. Toggle in Settings → Detection Engines. - Auto-Mute Low-Confidence Blocks — opt-in setting that silences blocks with
confidence < 60to voicemail instead of hard-rejecting. High-confidence hits (database, blocklist, STIR FAILED, heuristic ≥ 60) still hard-reject. Pure decision functionshouldSilence(silentVoicemailEnabled, autoMuteLowConfidenceEnabled, confidence)testable without a CallScreeningService. Default off. Toggle in Settings → Detection Engines. - 14 new JVM unit tests —
StirShakenTrustCheckerTest(8 cases including a full priority-ladder regression sweep),CallShieldScreeningServiceAutoMuteTest(6 cases covering every decision branch + boundary at confidence==60).
Changed
CallShieldScreeningService.respondBlock()now delegates response-shape decisions to a newbuildBlockResponse(prefs, confidence)helper. All three response shapes (silent-voicemail / auto-mute / hard-reject) share one reviewable decision table. Behavior-preserving for v1.6.3 users.CheckerPriorityladder extended withSTIR_SHAKEN_TRUSTED = 5_300. Existing slots unchanged.- ROADMAP "Current State" header rolled from stale v1.2.8 → v1.7.0; 10-item Iteration v1.7.0 backlog added.
Fixed
- Internal audit caught a P0 pre-ship: the first-cut priority slot for
STIR_SHAKEN_TRUSTEDwas placed at 8_700, aboveUSER_BLOCKLIST, which would have let a carrier-signed call override an intentional user block. Corrected to 5_300 before ship and locked with a regression test suite that asserts the invariant against every explicit user-rule slot.
Build
./gradlew testDebugUnitTest— 591 unit tests, green../gradlew assembleRelease— signed APK attached below.- minSdk 29, targetSdk 36, Kotlin 2.0.x, Compose BOM 2024.12.01.
Full CHANGELOG: https://github.com/SysAdminDoc/CallShield/blob/master/CHANGELOG.md
CallShield v1.6.3 - Hardening pass
Audit-driven hardening pass. Nine surgical correctness fixes across the screening pipeline, backup system, SMS receiver, and several services.
High
- PushAlertChecker — anchored digit match scoped to body only. The v1.6.1 lookbehind/lookahead prevented the caller's last-7 digits from matching inside a longer digit run, but the regex still ran against
title + "\n" + body. Since\nis a non-digit boundary, a standalone 7-digit run in a notification title (order ID, tracking number, delivery PIN) could still allow an unrelated caller whose last-7 happened to match. Digit match now body-only; phrase matches (your driver,out for delivery) still see title+body because they legitimately appear in titles. - BackupRestore — schedule fields now round-trip.
BackupWildcard/BackupKeywordpreviously droppedscheduleDays/scheduleStartHour/scheduleEndHouron export, so a time-gated rule restored from a v2 backup silently fired 24/7. Backup schema bumped to v3; readers accept v1–v3, pre-v3 imports default to "always active". - SmsReceiver — removed misleading
abortBroadcast(). CallShield isn't the default SMS app, so the call was either unordered or a no-op depending on OEM/API level — the message landed in the inbox either way. Comment rewritten to be explicit: we log, we do not suppress. - CallerIdOverlayService — publish session id before
addView. Lookup jobs scheduled during view construction could compare against an olderactiveSessionIdand mis-attribute results. Belt-and-braces fix.
Medium
- CallbackDetector — SQL
NUMBER LIKE '%<last7>'prefilter. Outgoing-24h and incoming-5min queries no longer scan the full window and post-filter in Kotlin; SQLite narrows the result set first. - CallShieldTileService — Mutex-serialized toggles. Two rapid QS-tile taps could both read the same state, both compute the opposite, and both write — leaving the toggle stuck.
- SpamMLScorer — sweep stale
.tmpon startup. Process kills betweenwriteTextandrenameToleft orphanspam_model_weights.json.tmpfiles that accumulated forever. - WildcardRule — regex path uses
numberVariants. The glob path already matched across+1, bare-digit, and1-prefixed forms; regex now does the same for parity.
Low
- SitTonePlayer —
AtomicBoolean.compareAndSet. Replaced racy@Volatile varcheck-then-set guard.
Tests
577 unit tests pass. Regression coverage added for:
- body-only digit scope on PushAlertChecker
- regex/glob parity on WildcardRule (new file,
WildcardRuleTest) - v3 backup schedule round-trip on BackupRestore
- NUMBER prefilter on CallbackDetector query builders
Follow-ups (known, not fixed this pass)
Real findings from the audit that deserve their own session — intentionally queued:
- ContactsContract observer (contact cache stale up to 60s after add)
collectAsStateWithLifecyclesweep across dashboard/screens- Backup restore Replace-vs-Merge UX
- Screening-service block logging keep-alive under memory pressure
- Split MainViewModel (28 StateFlows today)
- Extract
PhoneUtils(normalization duplicated in 13+ screens)
CallShield v1.6.2 - Maintenance release
Maintenance release. No app-code changes since v1.6.1 — bundles the refreshed spam database and a CI cadence fix.
Changed
- CI:
merge-reports.ymlnow runs weekly (Mon 08:00 UTC) instead of every 30 minutes. The hot-list refresh cadence was generating ~48 commits/day to master; the weekly build workflow already covers the same ground. No effect on shipped app behavior. - Data: refreshed community hot list and campaign-range aggregates.
Infrastructure
build.ymlnow chmodsgradlewon the runner and stops auto-uploading the unsigned CI APK to the Release (it still publishes the unsigned APK as a workflow artifact for inspection).
v1.6.1 - Post-release audit fixes
Patch release for v1.6.0 — seven defects caught by a code review
within hours of the v1.6.0 release. No new features, no schema changes,
same signing key as prior releases (in-place upgrade).
Critical
STIR/SHAKEN bypassed the manual-whitelist tier. The inline STIR
check in CallShieldScreeningService ran before the IChecker
pipeline, so a user's whitelist entry (emergency contact added via the
app's own UI, not the device address book) was hard-rejected whenever
the caller happened to fail carrier verification. STIR is now
StirShakenChecker at priority 8,500 — below MANUAL_WHITELIST
(10,000) and CONTACT_WHITELIST (9,000), above every block. A
trusted emergency contact on a non-STIR carrier rings through again.
High
- Push-alert number match anchored. A spam caller whose last 7
digits happened to appear inside an unrelated digit run (order ID,
tracking number, conference PIN) was being allowed through. Now
anchored with(?<!\d)…(?!\d)so only standalone digit runs count. - Trust-phrase list tightened. Bare
calendarregex dropped — used
to match any notification mentioning "calendar".outsidenow
requires a subject word (is outside,I'm outside,arriving outside) so weather notifications don't fire it. - Verification phrases gated by package. "Your verification code is
…" from Outlook or any non-messaging app no longer unblocks unrelated
callers. Only the four SMS-messaging packages that actually send
verification codes trigger the phrase. Calendar-reminder phrases
similarly gated to calendar apps.
Medium
util/Race.kt: a competitor that threw was synthesizing
onTimeoutas its "result" and, for callers where
decisive(onTimeout) == true, winning the race on failure. Now
uses a sealedOutcometype — failures only decrement the remaining
tally.- PushAlertRegistry opt-out atomicity: new
applyOptOuts(Set)
prunes cached alerts from newly-disabled packages before publishing
the new opt-out set, closing a window where concurrent screening
could read stale alerts from a just-disabled source. Defensive
HashSet(...)copy on every write. SystemBlockList.isBlockedclears its cache on SecurityException
(default-dialer role revoked mid-session) so a staletruefrom the
previous role session can't influence subsequent verdicts.
Low
CheckerPipeline.runnow bails before each checker when
ctx.timeLeftMillis() <= 0— cheap insurance against a future
slow checker eating the 5-second Android screening deadline.
Tests
11 new regression tests in PushAlertCheckerTest pinning the anchored
digit match and package-gated phrase rules so nobody accidentally
loosens them again.
Build
./gradlew assembleDebug— clean./gradlew testDebugUnitTest— all green (new + existing)./gradlew assembleRelease— clean, R8-minifiedapksigner verify— v2 signature confirmed
Upgrade
Same keystore / extension ID as v1.6.0 and prior. Sideload
CallShield-v1.6.1.apk over your existing install.
v1.6.0 - Peer-inspired track + A3 allowlist editor
What's new
Eight items ported from the strongest OSS Android call/SMS blockers
(SpamBlocker,
YetAnotherCallBlocker,
Saracroche,
Fossify Phone,
BlackList), plus the first
opening audit/hardening pass of the Opus 4.7 review cycle. Every
feature lands user-visible with settings UI.
Detection pipeline (A1)
Replaced the 140-line isSpam() waterfall with a priority-sorted IChecker pipeline. 13 checkers, first-non-null wins, testable in isolation. Every layer now has a stable priority (CheckerPriority.*) and a short snake_case name that lands in the blocked-call log.
Push-alert bridge (A3) — biggest false-positive fix
Notifications from 24 allowlisted apps (Uber, DoorDash, Amazon, FedEx, USPS, Gmail, Outlook, Calendar, Google/Samsung Messages, and more) can vouch for an unknown caller within 30 minutes. Direct number match in the alert body, or trust-phrase match ("your driver", "verification code"), allows the call through with priority between user-explicit blocks and the weaker heuristic layers.
Allowlist editor: modal bottom sheet in Settings with per-package switches, real app labels resolved via PackageManager, installed-first sort, and "Restore defaults". Opt-out semantics — future default additions propagate automatically. Clears cached alerts when a source is disabled.
System block-list (A4)
Read-only bridge to Android's BlockedNumberContract.BlockedNumbers. If you've marked a number as blocked in the stock Phone or Messages app, CallShield now respects it. Graceful SecurityException handling for non-default-dialer installs.
Length-locked range patterns (A5)
Saracroche-style # wildcards — +33162###### blocks a whole NPA-NXX in one rule. Pure character-index matching, no regex JIT. New Ranges tab with:
- Pattern overlap detection ("already covered by
+33#######") - Live coverage-count pill ("Covers 1,000,000 numbers")
- Safety rail rejecting patterns that cover >100M numbers
- Country-prefix variant generator so
+33612######also matches national-format0612345678
Per-rule schedule gating (A7)
Every rule type — wildcard/regex, range, and SMS keyword — can now be gated to specific days of the week and an hour window. Rule-add dialogs get a day-chip row + two hour pickers. List items show an "Active Mon–Fri · 09:00–17:00" pill. Schedule defaults to "not gating" so every pre-v1.6 rule keeps its existing behaviour.
Budget-aware race utility (A2)
New util/Race.kt — races N suspend blocks against a hard budget, returns the first decisive result, cancels losers via structured concurrency. Foundation for reputation-API work under the 5-second CallScreeningService deadline.
Opening audit pass (hardening)
SpamMLScorer: single-callverdict()replaces double-scoring (was ~2x cost on the hot path);parseModelis now pure and commits atomically so a corrupt sync can't briefly expose default weights.- CoroutineScope leaks in
SpamActionReceiverandCallShieldWidget— both now useCallShieldApp.appScope. CallShieldScreeningService: single prefs snapshot threaded through the entire call chain;respondToCallnow runs before logging so Android never auto-allows on service-unbind races.- Room
LIKEwildcard injection insearchNumbers/searchLog— user input is nowESCAPE '\'-escaped. - Various smaller: dead code removal, scope-aware capture in the notification listener, cleaner atomic ML state.
Database
DB_VERSION now 9. Three additive migrations (v6→v7, v7→v8, v8→v9) layered on top of v1.5. Every migration is additive (ALTER TABLE … ADD COLUMN … DEFAULT 0 or CREATE TABLE IF NOT EXISTS) — zero risk for existing data.
Build & signing
./gradlew assembleDebug— compiles clean./gradlew testDebugUnitTest— all unit tests pass./gradlew assembleRelease— signed release APK attached below- APK v2 signature verified via
apksigner verify
Install
Download CallShield-v1.6.0.apk below and sideload.
CallShield v1.5.2 — Crash fixes
Hotfix for v1.5.0 launch crash and Recent tab crash.
Fixes
- Crash on launch: Added missing R8/proguard keep rules for
GitHubDataSourceMoshi payload inner classes (HotListPayload,HotRangesPayload,SpamDomainsPayload,HotListEntry,HotRangeEntry). Without them, R8 stripped Kotlin metadata and Moshi threwCannot serialize abstract classduring MainViewModel construction. - Recent tab crash:
LazyColumnkey built from(number, date, type)collided on dual-SIM duplicates and MMS group rows. Key now includes the list index. - Also hardened:
-keepattributes RuntimeVisibleAnnotations,Signature,InnerClasses,EnclosingMethod+-keep class kotlin.Metadatato prevent this class of regression.
CallShield v1.5.0
Engineering Audit Release
12 bug fixes, 4 performance improvements, 2 security hardening measures across 20 files.
Fixed
- SpamMLScorer race condition — atomic ModelState snapshot prevents half-updated model reads during concurrent scoring
- WildcardRule regex injection — full metacharacter escaping; previously
.,(,), etc. were live regex - Frequency auto-escalation — 7-day sliding window replaces unbounded count that false-positive'd on legitimate callers
- SyncWorker — permanent failures (HTTP 404) no longer silently reported as success to WorkManager
- SpamActionReceiver — SupervisorJob prevents coroutine scope crash before pendingResult.finish()
- MainViewModel scan guard — TOCTOU race allowed duplicate concurrent call log / SMS inbox scans
- isWangiriCountryCode — Caribbean +1 NPAs (876 Jamaica, 284 BVI, 649 Turks & Caicos) now correctly detected
- RcsNotificationListener — idiomatic scope cancellation
Performance
- Shared OkHttpClient singleton (7 instances → 1 with derived builders)
- Room query caching for prefixes, wildcards, keyword rules on the 5-second call screening path
- Single DataStore read in isSpam() replaces 8+ Flow .first() collectors
- CampaignDetector O(n) eviction replaces O(n log n) sort
Hardening
- WildcardRule: user regex patterns capped at 200 chars (ReDoS protection)
- SmsContentAnalyzer: URL regex length-capped at 2048 chars
CallShield v1.4.0 — Smart labels, silent voicemail, FTC report, emergency contacts, block reasoning
CallShield v1.4.0 — Five user-facing features
Minor-version bump shipping five new capabilities in one release.
A. Smart call labels
Every blocked call is now labeled with a specific category — Debt Collector · Political · Robocall · Scam · Phishing · Telemarketer · Wangiri · Survey · Business · Unknown — shown prominently in the Number Detail hero and inline on every blocked-log row.
The resolver prioritizes database type tags (most trustworthy, came from labeled community/FCC/FTC data), then matchSource/layer, then heuristic keywords, then ML confidence-gated hits (≥80% only). Conservative on purpose — mislabeling a legitimate bank as "Scam" erodes trust faster than a missing label. 13 unit tests lock down the priority order.
B. Silent voicemail mode
New Silent Voicemail Mode toggle in Settings → Detection Engine. When enabled, blocked calls route silently to voicemail — your phone doesn't ring, the caller hears normal rings and reaches voicemail, and the disruption disappears. Off by default; users who want the missed-call entry as an audit trail keep hard reject.
C. One-tap FTC fraud report
Every Number Detail screen now has a "Report to FTC" button that copies the number to your clipboard, opens reportfraud.ftc.gov in your browser, and shows a Toast telling you to paste into the form's phone-number field. FTC complaints drive actual enforcement — this turns a tedious multi-minute task into about 10 seconds. Keeps CallShield's no-telemetry promise: we don't report anything for you, we just make it drastically easier for you to report.
D. "Why was this blocked?"
Every Number Detail screen now shows a plain-English narrative explaining exactly which detection layer fired, what heuristic reasons contributed, and the model's confidence. Example:
Flagged by the heuristic engine at 78% confidence.
• Matched at detection layer 11 (heuristics).
• high spam npa
• voip spam range
• neighbor spoof
Covers all 15+ detection layers plus the allow-through sources (emergency contact, manual whitelist, contact whitelist, recently dialed, repeated urgent caller, SMS context trust). Reconstructed at view time from already-persisted fields — zero hot-path cost.
E. Emergency contacts
New Emergency flag on whitelist entries. An emergency contact bypasses the blocklist, quiet hours, aggressive mode, and every other filter — it always rings through, no matter what. Perfect for your kid's school, your doctor, or an elder-care facility.
- Whitelist tab now renders emergency entries first with a red accent + star toggle + EMERGENCY badge.
- Add-to-Whitelist dialog gains an Emergency checkbox that flips the confirm button's color and copy.
- Blocked log distinguishes
emergency_contactmatches from regularmanual_whitelistallows so you can see if a call slipped through via emergency status. - Room DB v5 → v6 via explicit
MIGRATION_5_6— existing whitelist entries keep their current behavior (isEmergency defaults to 0).
Under the hood
versionCode19 → 20,versionName1.3.0 → 1.4.0.- Room DB v5 → v6 (first non-legacy schema bump with a proper Migration object, thanks to the guard added in v1.2.12).
- 3 new data-layer files:
CallCategory.kt,BlockReasoning.kt,ReportFraudHelper.kt. - 21 new unit tests (
CallCategoryResolverTest,BlockReasoningTest). - 28+ new string resources, translation-ready.
Verification (all green)
./gradlew :app:testDebugUnitTest— 21 new tests + existing suite./gradlew :app:assembleDebug./gradlew :app:lintDebug./gradlew :app:compileDebugAndroidTestKotlin./gradlew :app:assembleRelease(R8 + resource shrink, signed)
Install
Download CallShield-v1.4.0.apk below. Same signing key as previous v1.x.y — upgrades in place. The Room migration runs once on first launch; your existing whitelist, blocklist, rules, and call log are preserved.
Requires Android 10 (API 29) or higher. Target SDK 36.
CallShield v1.3.0 — Crash reporter + test infrastructure
CallShield v1.3.0
Minor-version bump — not another audit round, but infrastructure that catches bugs static review can't.
Local crash reporter
- New
CrashReporterinstalls aThread.setDefaultUncaughtExceptionHandleron app startup (before any other init) so crashes during startup hot paths still get captured. - Each crash serializes to
filesDir/crashes/crash_<epoch>.txtwith timestamp, build info, thread, and a bounded cause chain. - Rotates to the 5 most recent reports.
- Chains to the previous default handler — never swallows the crash signal.
- Zero off-device telemetry. We do not ship Crashlytics, Sentry, or Bugsnag — that conflicts with CallShield's on-device-only stance. Instead, a new "Share Last Crash Log" Quick Link in the More tab opens a FileProvider share intent so you can attach a crash report to a bug issue on your own terms.
Instrumentation test suite (new androidTest source set)
Three test classes covering load-bearing code paths that can't be exercised from pure-JVM tests:
ThemePrimitivesTest—PremiumCardrenders + forwards clicks,SectionHeaderrenders its label,accentGlowcomposes without throwing. These primitives are used on every screen.DashboardStatusBadgeTest— pure-function state-machine coverage ofbuildDashboardStatusModel(shield-active logic, hero mode transitions, optional-vs-required setup counting).CrashReporterInstrumentedTest— end-to-end file IO + FileProvider share-intent shape +clearAll()correctness.
Emulator CI workflow
.github/workflows/instrumented.yml runs connectedDebugAndroidTest on macos-latest via reactivecircus/android-emulator-runner@v2 with AVD snapshot caching. Triggers on PR + master push + manual dispatch. Path-filtered so doc-only commits don't burn CI credits.
Hot-path microbenchmarks
HotPathBenchmarkTest enforces regression ceilings on the detection hot paths:
| Component | Iterations | Ceiling |
|---|---|---|
WildcardRule.matches |
10,000 | 500 ms |
CampaignDetector.record + isActive |
1,000 cycles | 200 ms |
SpamMLScorer.score (GBT or LR) |
1,000 | 300 ms |
SpamHeuristics pure checks |
1,000 cycles | 100 ms |
20x headroom over measured performance. Ceilings tunable via -Dcallshield.benchHeadroom=2.0 for slow CI runners.
Under the hood
versionCode18 → 19,versionName1.2.15 → 1.3.0 (minor bump — new user-visible capability).buildFeatures.buildConfigre-enabled so the crash header can readBuildConfig.VERSION_NAME/VERSION_CODE.file_paths.xmlexposesfilesDir/crashes/through FileProvider.- New
androidTestdependencies inlibs.versions.toml:androidx.test.ext.junit,androidx.test.runner,androidx.test.core,espresso-core,compose-ui-test-junit4,compose-ui-test-manifest.
Verification (all green)
./gradlew :app:testDebugUnitTest— 4 new benchmark ceilings + 2 CrashReporter tests + existing suite./gradlew :app:assembleDebug./gradlew :app:lintDebug./gradlew :app:compileDebugAndroidTestKotlin— instrumented suite compiles clean./gradlew :app:assembleRelease(R8 + resource shrink, signed)
Install
Download CallShield-v1.3.0.apk below. Same signing key as previous v1.2.x — upgrades in place.
Requires Android 10 (API 29) or higher. Target SDK 36.