An Android app that watches the cell your phone is attached to and tells you when something about it
changes. Everything stays on the device: it holds no INTERNET permission, so it cannot send your
data anywhere even if a future change tried to.
What this app is not. It cannot read baseband signalling, it cannot see your encryption state, it cannot detect silent SMS, and it cannot tell you that you are being targeted by an IMSI catcher. Android gives no application access to any of that. Earlier versions of this app claimed all four. See Limitations and docs/LESSONS.md.
Every five seconds while the app is open, it reads the registered cell from Android's
TelephonyManager and compares that reading against everything it has seen this session.
- Cell readings — identity, tracking/location area, PLMN, signal level and neighbour count, each
shown only when the platform reports it. Anything unreported shows as
—. - Comparative anomaly detection — five rules, all of which look at change rather than at a single reading. Radio access dropping from LTE/NR to GSM/UMTS; a neighbour list that was populated and is now empty; a cell that kept its identity but changed its area code; a signal far above that cell's own median for the session. Each finding shows the readings it came from, so you can disagree with it.
- Integrity score — 100 minus the weights of the rules that fired. It reads
—, not 100%, until the session has enough readings to compare against. - Observation log — one row per scan, including scans that produced nothing, with the reason. Searchable, copyable, exportable via the share sheet.
- Cell registry — every identified cell, how often it has been seen, and where the device was standing at the time.
- Network link facts — interface, addresses, DNS servers, private-DNS state, VPN transport, SIM
country versus network country. Read from
LinkProperties; nothing is measured or traced.
| Signal | Fires when | Severity |
|---|---|---|
| Legacy radio access in use | Serving cell is 2G/3G | Informational |
| Radio access downgraded | Now 2G/3G, after LTE/NR earlier in the session | Warning |
| Neighbour list emptied | Zero neighbours, having reported ≥2 earlier | Warning |
| Area code changed on a stable cell | Same cell ID, different TAC/LAC | Warning |
| Signal far above baseline | ≥20 dB above this cell's own session median | Elevated |
Every one of these has an innocent explanation that is more likely than interception — leaving LTE coverage, moving between cells, a carrier reconfiguring a site. The app says so on the screen. Treat findings as things to look at, not as conclusions.
Stated here rather than left to be discovered.
- Foreground only. Monitoring runs in a
ViewModel, so it stops when you leave the app or the screen turns off. Gaps in the log are expected. There is no background service. - No baseband access. No Android app can read RRC or NAS signalling on an unmodified device,
with any permission. The observation log is a log of what
TelephonyManagerreturned. - No encryption visibility. Android exposes no ciphering indicator. The app cannot tell you whether your traffic is encrypted over the air.
- No silent SMS detection. Type-0 and class-0 messages are handled below the application layer.
- No traceroute or latency measurement. An unrooted app has no raw sockets and no control over IP TTL.
- Cell positions are device positions. A handset can locate itself, not a transmitter.
- Per-session baselines only. A cell seen yesterday is not compared against today's reading.
- A real IMSI catcher may produce no signal here at all. Modern interception does not have to force a downgrade or empty a neighbour list. Absence of findings is not evidence of absence.
play-services-locationis a proprietary Google dependency — the only one. It provides the fused location fix. There is no Firebase, no analytics and no other Google component.
- No
INTERNETpermission. The app cannot open a network connection. This is enforced by Android, not promised in this file, and it is checkable in one line ofAndroidManifest.xml. android:allowBackup="false", with cloud backup and device-transfer rules that exclude the app's storage. Earlier versions had backup enabled with empty rules, so the log was copied to the user's Google Drive.- No analytics, no ads, no backend, no account.
- The database is not encrypted. It is a plain Room/SQLite file in app-private storage. Earlier versions of the privacy policy called it encrypted; it never was. If your threat model includes someone with access to your unlocked device, use the in-app clear function.
Permissions and why: READ_PHONE_STATE and ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION are both
required by Android before it will report cell identity at all; ACCESS_NETWORK_STATE for the link
facts; POST_NOTIFICATIONS to raise an alert. Full detail in PRIVACY_POLICY.md.
- Language: Kotlin
- UI: Jetpack Compose, Material 3
- Pattern: MVVM, with the detection rules isolated in a pure-Kotlin
detectorpackage - Storage: Room (KSP), schema exported to
app/schemas/, real migrations - Async: Coroutines,
StateFlow,collectAsStateWithLifecycle
app/src/main/java/com/techluddite/cellhoneypot/
scanner/ TelephonyManager, ConnectivityManager and location readers
detector/ AnomalyDetector — pure Kotlin, no Android imports, unit-tested
data/ Room entities, DAO, migrations, repository
viewmodel/ Watchdog loop and monitor state
ui/ Compose screens and components
See docs/ARCHITECTURE.md for the invariants and the reasoning behind the detection rules and their weights.
Requires JDK 17+ and an Android SDK with API 36. The Gradle wrapper is checked in; earlier versions of this repository had none, and could not be built from a clean checkout at all.
git clone https://github.com/TechLuddite/Cellular-Honeypot-Detector.git
cd Cellular-Honeypot-Detector
./gradlew :app:testDebugUnitTest # detector rules + Room migration test
./gradlew :app:lintDebug # runs with abortOnError = true
./gradlew :app:assembleDebugassembleRelease requires a real upload keystore (KEYSTORE_PATH, STORE_PASSWORD, KEY_ALIAS,
KEY_PASSWORD, or my-upload-key.jks in the project root) and fails without one. It used to fall
back to the checked-in debug key.
This project was generated in Google AI Studio, and versions 1–9 shipped detection logic that
invented its own inputs: a Random cell ID, sixteen random bytes displayed as a baseband hex dump,
a hardcoded ciphering algorithm, hardcoded network latencies, a nine-tenths-constant signal chart
and a default GPS coordinate in San Francisco. Version 10 is the result of an audit that removed all
of it.
docs/LESSONS.md is the postmortem — ten recurring failure shapes, each with the instance that revealed it and a tell for spotting the same shape elsewhere. It is the most useful file in this repository.
- PayPal: paypal.me/techluddite
- GitHub: github.com/TechLuddite
- Other projects: Cropalot · Universal-AI · NetReady
MIT. Copyright (c) 2026 TechLuddite.