{"id":51155993,"url":"https://github.com/renaudallard/regexphone","last_synced_at":"2026-06-26T10:30:37.315Z","repository":{"id":358061489,"uuid":"1239640872","full_name":"renaudallard/regexphone","owner":"renaudallard","description":"block phone numbers with regexes","archived":false,"fork":false,"pushed_at":"2026-05-15T15:06:34.000Z","size":1295,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-15T15:28:30.399Z","etag":null,"topics":["android","call-blocker","call-screening","jetpack-compose","kotlin","privacy","regex","spam-blocker"],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/renaudallard.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-15T09:41:51.000Z","updated_at":"2026-05-15T15:06:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/renaudallard/regexphone","commit_stats":null,"previous_names":["renaudallard/regexphone"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/renaudallard/regexphone","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fregexphone","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fregexphone/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fregexphone/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fregexphone/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/renaudallard","download_url":"https://codeload.github.com/renaudallard/regexphone/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/renaudallard%2Fregexphone/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34813782,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","call-blocker","call-screening","jetpack-compose","kotlin","privacy","regex","spam-blocker"],"created_at":"2026-06-26T10:30:36.713Z","updated_at":"2026-06-26T10:30:37.309Z","avatar_url":"https://github.com/renaudallard.png","language":"Kotlin","funding_links":["https://www.paypal.me/RenaudAllard"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"branding/playstore/feature_graphic.png\" alt=\"RegexPhone\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eRegexPhone\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  Block or allow incoming calls with regular expressions.\u003cbr/\u003e\n  Native Android \u003ccode\u003eCallScreeningService\u003c/code\u003e, no background daemons, no contacts permission, no network.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/minSdk-31-3DDC84?logo=android\u0026logoColor=white\" alt=\"minSdk 31\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Kotlin-2.0-7F52FF?logo=kotlin\u0026logoColor=white\" alt=\"Kotlin 2.0\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Jetpack%20Compose-Material%203-4285F4?logo=jetpackcompose\u0026logoColor=white\" alt=\"Jetpack Compose\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/build-Gradle%208.10-02303A?logo=gradle\u0026logoColor=white\" alt=\"Gradle 8.10\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-BSD--2--Clause-blue\" alt=\"BSD 2-Clause license\" /\u003e\n  \u003cimg src=\"https://img.shields.io/github/downloads/renaudallard/regexphone/total?logo=github\u0026logoColor=white\u0026label=downloads\u0026cb=2\" alt=\"GitHub downloads\" /\u003e\n\u003c/p\u003e\n\n---\n\n## What it does\n\nFor every incoming call, RegexPhone matches the caller's phone number against your rules and either lets it ring, rejects it, or silences the ringtone. Each rule is a regular expression with an action (`BLOCK`, `SILENCE`, or `ALLOW`) and, for block rules, control over whether the missed-call notification should appear.\n\n## Highlights\n\n- **Three actions per rule.** `BLOCK` rejects the call; `SILENCE` mutes the ringtone but lets the call still hit the call log and notification shade; `ALLOW` whitelists.\n- **Per-block flag.** Skip the missed-call notification, per rule. Android itself always records blocked calls in the call log; only carrier and system screeners may suppress that, so RegexPhone does not pretend to offer it.\n- **Predictable precedence.** Allow beats block beats silence; otherwise the call is allowed. The verdict does not depend on rule order; only the notification flag comes from the first matching block rule.\n- **Live tester.** The edit screen previews, as you type, the decision the service would take for a sample number across all enabled rules, naming the rule that decides.\n- **Import / Export.** Save the full rule set to a JSON file via Storage Access Framework, restore it on another device, or merge two sets together. No permissions needed.\n- **Simple storage.** Rules are JSON in `SharedPreferences`, kept in device-protected storage so screening already works between a reboot and the first unlock. Device-protected storage is encrypted with a device key rather than the lock-screen credential.\n- **No background services, no contacts permission, no network access.**\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"branding/screenshots/rules-list.png\" alt=\"Rules list\" width=\"300\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"branding/screenshots/edit-rule.png\" alt=\"Edit rule\" width=\"300\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eLeft: rules list with role-status banner. Right: edit screen with the per-rule notification toggle.\u003c/em\u003e\u003c/p\u003e\n\n## How matching works\n\n| Aspect | Behaviour |\n| --- | --- |\n| Source | `Call.Details.handle.schemeSpecificPart`, URI-decoded |\n| Number forms | each rule is tried against the string as delivered by the carrier, its separator-stripped form, and its E.164 form (derived from the current network or SIM country); the rule matches if any form matches |\n| Hidden / withheld numbers | match as the empty string; block them with `^$` |\n| Match function | `Matcher.find()` (substring); anchor with `^` and `$` for whole-number match |\n| Invalid regex | never matches; the editor refuses to save it |\n\nFor each incoming call:\n\n1. If any enabled `ALLOW` rule matches, the call is allowed.\n2. Else if any enabled `BLOCK` rule matches, the call is rejected. The **skip notification** flag of the *first* matching block rule applies.\n3. Else if any enabled `SILENCE` rule matches, the call rings silently (no audible ringtone), but the call log and notifications are unaffected.\n4. Otherwise the call is allowed.\n\n## Example rules\n\n\u003e All numbers below use the NANP fictional ranges (`555` exchange in any area code, or area code `555`) so they cannot belong to any real subscriber.\n\n| Pattern | Action | What it does |\n| --- | --- | --- |\n| `^\\+12025550123$` | `BLOCK` | Block exactly one specific number. Substitute the real number from your call log. |\n| `^\\+44` | `BLOCK` | Block every call from a country (here `+44` is the UK). Works for any country code. |\n| `^$` | `BLOCK` | Block withheld / hidden numbers — Android delivers an empty string in that case. |\n| `^\\+1555\\d{7}$` | `BLOCK` | Block a whole carrier or number range. `\\d{7}` matches the seven digits after the fixed `+1555` prefix. |\n| `^\\+32`\u003cbr/\u003e`.*` | `ALLOW`\u003cbr/\u003e`BLOCK` | Two rules together: whitelist a country (Belgium), reject everything else. `.*` matches any sequence including the empty string. |\n| `^\\+1` | `SILENCE` | Mute the ringtone for calls from a country. The call still hits the call log and notification shade. |\n\n## Install\n\nGrab the latest signed APK from the [Releases page](https://github.com/renaudallard/regexphone/releases/latest), then:\n\n```sh\nadb install -r regexphone-X.Y.Z.apk\n```\n\nOn first launch tap **Set as default** in the status card and accept the system dialog; the card turns green once the role is granted.\n\n## Build from source\n\n| Tool | Version |\n| --- | --- |\n| JDK | 17 or 21 |\n| Android SDK | Platform 35 and Build-tools 35.0.x |\n| Gradle | 8.10.2 (via wrapper) |\n\nUse JDK 17 or 21. Gradle 8.10.2's embedded Kotlin compiler cannot parse the JDK 25 version string and fails with `IllegalArgumentException: 25.0.3`. If your system default is newer, point the build at a supported JDK:\n\n```sh\nJAVA_HOME=/usr/lib/jvm/java-21-openjdk-arm64 ./gradlew ...\n```\n\nor set `org.gradle.java.home` in `~/.gradle/gradle.properties`.\n\n```sh\ngit clone https://github.com/renaudallard/regexphone.git\ncd regexphone\n\n# One-time, only if gradle/wrapper/gradle-wrapper.jar is missing:\ngradle wrapper --gradle-version 8.10.2\n\n./gradlew testDebugUnitTest\n./gradlew assembleDebug\n```\n\nThe APK lands at `app/build/outputs/apk/debug/regexphone-X.Y.Z.apk` (the build renames every variant after the version).\n\n### Release builds\n\n`./gradlew assembleRelease` produces `app/build/outputs/apk/release/regexphone-X.Y.Z.apk`. The build script picks up signing credentials from Gradle properties (typically `~/.gradle/gradle.properties`):\n\n```\nREGEXPHONE_KEYSTORE_PATH=/absolute/path/to/keystore.jks\nREGEXPHONE_KEYSTORE_PASSWORD=...\nREGEXPHONE_KEY_ALIAS=regexphone\nREGEXPHONE_KEY_PASSWORD=...\n```\n\nIf `REGEXPHONE_KEYSTORE_PATH` is unset or points to a missing file, `assembleRelease` still works and emits the same `regexphone-X.Y.Z.apk`, just unsigned. Generate a fresh keystore with:\n\n```sh\nkeytool -genkeypair -keystore ~/.keystores/regexphone-release.jks \\\n  -storetype PKCS12 -alias regexphone -keyalg RSA -keysize 2048 \\\n  -validity 36500 -dname \"CN=Your Name, O=RegexPhone\"\n```\n\nKeystore files (`*.jks`, `*.keystore`) are gitignored. Back the keystore up off-device; losing it means you can never sign a follow-up release with the same identity.\n\n\u003cdetails\u003e\n\u003csummary\u003eDebian arm64 setup (the official \u003ccode\u003egoogle-android-*-installer\u003c/code\u003e packages are amd64-only)\u003c/summary\u003e\n\n```sh\nsudo apt install openjdk-21-jdk\ncurl -LO https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip\nmkdir -p ~/Android/Sdk/cmdline-tools\nunzip commandlinetools-linux-13114758_latest.zip -d ~/Android/Sdk/cmdline-tools\nmv ~/Android/Sdk/cmdline-tools/cmdline-tools ~/Android/Sdk/cmdline-tools/latest\n\nexport JAVA_HOME=/usr/lib/jvm/java-21-openjdk-arm64\nexport PATH=~/Android/Sdk/cmdline-tools/latest/bin:$PATH\n\nyes | sdkmanager --licenses\nsdkmanager 'platforms;android-35' 'build-tools;35.0.1' 'platform-tools'\n\necho \"sdk.dir=$HOME/Android/Sdk\" \u003e local.properties\n```\n\nDebian's `gradle` is 4.4.1, which is too old to bootstrap AGP 8. Either copy `gradle/wrapper/gradle-wrapper.jar` from a host that has it, or grab a standalone Gradle 8.10.2:\n\n```sh\ncurl -LO https://services.gradle.org/distributions/gradle-8.10.2-bin.zip\nunzip gradle-8.10.2-bin.zip -d ~/Android\n~/Android/gradle-8.10.2/bin/gradle wrapper --gradle-version 8.10.2\n```\n\n\u003c/details\u003e\n\n### Play Store bundle (AAB)\n\nGoogle Play distributes an Android App Bundle (`.aab`), not an APK. Build it with:\n\n```sh\n./gradlew bundleRelease\n```\n\nThe bundle lands at `app/build/outputs/bundle/release/app-release.aab`, signed with the same `REGEXPHONE_*` credentials as the APK. The `outputFileName` rename only applies to APK outputs, so the bundle keeps its default name. Under Play App Signing this keystore acts as the upload key; Google manages the final distribution signing key.\n\n## Project layout\n\n```\napp/src/main/java/it/allard/regexphone/\n├── MainActivity.kt\n├── data/\n│   ├── Rule.kt                      data class + compiled-Pattern cache\n│   ├── RegexGuard.kt                watchdog-bounded regex matching\n│   ├── RuleIO.kt                    pure encode / decode / merge helpers\n│   └── RuleRepository.kt            singleton, SharedPreferences-backed\n├── service/\n│   └── FilterCallScreeningService.kt    pure decide() + the Android binding\n└── ui/\n    ├── Theme.kt\n    ├── RulesListScreen.kt           list + role-status card + FAB + menu\n    └── EditRuleScreen.kt            form + live tester\n```\n\nTests live under `app/src/test/java/it/allard/regexphone/`: `DecideTest.kt` exercises `FilterCallScreeningService.decide()`, `RuleIOTest.kt` covers JSON round-trip, import filtering, salvage and id reassignment, and `RegexGuardTest.kt` covers the match watchdog. All run without Android stubs.\n\n## Limitations\n\n- Only incoming calls are screened. The platform also hands outgoing calls to the screening service (for caller-ID purposes) but ignores any screening response for them; RegexPhone answers those with a no-op without evaluating rules.\n- Blocked calls always appear in the call log with the *blocked* type. The `CallScreeningService` API reserves call-log suppression for carrier and system apps.\n- **Rules are deliberately excluded from Google cloud backup and device-to-device transfer**: the rule set reveals who you block, so it never leaves the device on its own. After migrating to a new phone the app starts empty; carry rules over with Export on the old device and Import on the new one.\n- **Callers already in your contact list bypass the regex entirely.** Android's telecom layer short-circuits `CallScreeningService` when the incoming number matches a saved contact: it returns *allow* without ever invoking the screening service, so no rule of yours can run. To block a number that is in contacts, delete (or temporarily delete) the contact entry first. This is by design at the system level — the official `CallScreeningService` documentation states the service is \"called when a new incoming or outgoing call is added which is not in the user's contact list.\"\n- `java.util.regex.Pattern` has no built-in match timeout, and a regex with nested quantifiers like `(a+)+b` can backtrack catastrophically. RegexPhone runs every match on a watchdog thread with a 1 second deadline and bounds a whole screening pass to 3.5 seconds, so the response always stays within Telecom's ~5 second budget; the ALLOW pass is capped below that so a heavy whitelist cannot starve the block rules. A pattern that misses its full deadline is treated as *no match* and skipped for the rest of the process lifetime; the live tester keeps a separate blacklist, so experimenting in the editor cannot disable a saved rule for real calls. A runaway match cannot be cancelled mid-flight, so it is dropped to minimum priority and abandoned: it frees its slot at once, and a fresh match is never blocked by an un-stoppable one. At most six matches run at once; abandoned runaways are bounded by a separate, larger cap that a crafted set of distinct catastrophic patterns can fill only at the cost of background CPU.\n- An import is limited to 1 MB and 1000 rules; a larger file is rejected rather than truncated, and a merge that would push the stored set past 1000 rules is blocked (replace instead). Every enabled rule is matched on each incoming call within a fixed deadline, so the cap keeps the whole rule set evaluable.\n\n---\n\n## Support this project\n\nIf you find RegexPhone useful, you can support development:\n\n[![PayPal](https://img.shields.io/badge/PayPal-Donate-blue.svg?logo=paypal)](https://www.paypal.me/RenaudAllard)\n\n## License\n\nBSD 2-Clause \"Simplified\" License. Copyright (c) 2026, Renaud Allard \u003crenaud@allard.it\u003e. See [LICENSE](LICENSE) for the full text. Every Kotlin source file carries the same header.\n\n## Branding\n\nThe icon set under `branding/` is generated from `branding/source/*.svg` and theme color `#5E5BFF`. The monochrome layer is wired up for Android 13+ themed icons.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenaudallard%2Fregexphone","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frenaudallard%2Fregexphone","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frenaudallard%2Fregexphone/lists"}