Skip to content

Releases: SysAdminDoc/CallShield

v1.7.1 \u2014 caller-ID race + push-allow feedback

30 Apr 00:22

Choose a tag to compare

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

24 Apr 05:48

Choose a tag to compare

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 PASSED attestation on the calling number. Priority slot STIR_SHAKEN_TRUSTED = 5_300 sits 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 setting KEY_STIR_TRUSTED_ALLOW defaults on. Toggle in Settings → Detection Engines.
  • Auto-Mute Low-Confidence Blocks — opt-in setting that silences blocks with confidence < 60 to voicemail instead of hard-rejecting. High-confidence hits (database, blocklist, STIR FAILED, heuristic ≥ 60) still hard-reject. Pure decision function shouldSilence(silentVoicemailEnabled, autoMuteLowConfidenceEnabled, confidence) testable without a CallScreeningService. Default off. Toggle in Settings → Detection Engines.
  • 14 new JVM unit testsStirShakenTrustCheckerTest (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 new buildBlockResponse(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.
  • CheckerPriority ladder extended with STIR_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_TRUSTED was placed at 8_700, above USER_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

24 Apr 04:48

Choose a tag to compare

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 \n is 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/BackupKeyword previously dropped scheduleDays/scheduleStartHour/scheduleEndHour on 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 older activeSessionId and 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 .tmp on startup. Process kills between writeText and renameTo left orphan spam_model_weights.json.tmp files that accumulated forever.
  • WildcardRule — regex path uses numberVariants. The glob path already matched across +1, bare-digit, and 1-prefixed forms; regex now does the same for parity.

Low

  • SitTonePlayer — AtomicBoolean.compareAndSet. Replaced racy @Volatile var check-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)
  • collectAsStateWithLifecycle sweep 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

24 Apr 04:25

Choose a tag to compare

Maintenance release. No app-code changes since v1.6.1 — bundles the refreshed spam database and a CI cadence fix.

Changed

  • CI: merge-reports.yml now 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.yml now chmods gradlew on 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

22 Apr 16:10

Choose a tag to compare

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 calendar regex dropped — used
    to match any notification mentioning "calendar". outside now
    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
    onTimeout as its "result" and, for callers where
    decisive(onTimeout) == true, winning the race on failure. Now
    uses a sealed Outcome type — 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.isBlocked clears its cache on SecurityException
    (default-dialer role revoked mid-session) so a stale true from the
    previous role session can't influence subsequent verdicts.

Low

  • CheckerPipeline.run now 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-minified
  • apksigner 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

22 Apr 15:38

Choose a tag to compare

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-format 0612345678

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-call verdict() replaces double-scoring (was ~2x cost on the hot path); parseModel is now pure and commits atomically so a corrupt sync can't briefly expose default weights.
  • CoroutineScope leaks in SpamActionReceiver and CallShieldWidget — both now use CallShieldApp.appScope.
  • CallShieldScreeningService: single prefs snapshot threaded through the entire call chain; respondToCall now runs before logging so Android never auto-allows on service-unbind races.
  • Room LIKE wildcard injection in searchNumbers / searchLog — user input is now ESCAPE '\'-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

16 Apr 00:53

Choose a tag to compare

Hotfix for v1.5.0 launch crash and Recent tab crash.

Fixes

  • Crash on launch: Added missing R8/proguard keep rules for GitHubDataSource Moshi payload inner classes (HotListPayload, HotRangesPayload, SpamDomainsPayload, HotListEntry, HotRangeEntry). Without them, R8 stripped Kotlin metadata and Moshi threw Cannot serialize abstract class during MainViewModel construction.
  • Recent tab crash: LazyColumn key 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.Metadata to prevent this class of regression.

CallShield v1.5.0

15 Apr 22:59

Choose a tag to compare

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

12 Apr 22:21

Choose a tag to compare

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_contact matches from regular manual_whitelist allows 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

  • versionCode 19 → 20, versionName 1.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

12 Apr 21:50

Choose a tag to compare

CallShield v1.3.0

Minor-version bump — not another audit round, but infrastructure that catches bugs static review can't.

Local crash reporter

  • New CrashReporter installs a Thread.setDefaultUncaughtExceptionHandler on app startup (before any other init) so crashes during startup hot paths still get captured.
  • Each crash serializes to filesDir/crashes/crash_<epoch>.txt with 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:

  • ThemePrimitivesTestPremiumCard renders + forwards clicks, SectionHeader renders its label, accentGlow composes without throwing. These primitives are used on every screen.
  • DashboardStatusBadgeTest — pure-function state-machine coverage of buildDashboardStatusModel (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

  • versionCode 18 → 19, versionName 1.2.15 → 1.3.0 (minor bump — new user-visible capability).
  • buildFeatures.buildConfig re-enabled so the crash header can read BuildConfig.VERSION_NAME / VERSION_CODE.
  • file_paths.xml exposes filesDir/crashes/ through FileProvider.
  • New androidTest dependencies in libs.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.