Skip to content

Repository files navigation

5G Scanner Android

Android/Kotlin app for building a real 5G/LTE coverage data set from Android phones.

5G Scanner is meant to be installed, granted consent, and then left to work quietly in the background. While scanning is running, it records mobile signal quality, location, time, and network context so coverage can be mapped over time in the areas where participants actually work and travel.

The app focuses on 5G coverage, while also collecting LTE data when that helps explain the real mobile experience. It only collects data; it is not a navigation app, speed test, or network optimization tool.

Current Status

Planning and project setup plus a simulator-friendly Android scanner prototype.

The product direction is a consent-led background coverage collector. Most users should only need to grant consent, keep the app installed, and stop scanning when they are off duty or want to save battery. Engineers can inspect recorded samples and tune settings, but the app should stay simple for voluntary crowdsourced participation.

Storage is local-first, with reporting controlled by the app's reporting setting. Reporting currently builds capped JSONL batches from queued rows, posts them to the configured reporting endpoint, retries failures with backoff, and recovers interrupted in-flight sends on later reporting triggers.

The current prototype version is 0.1.0. Use SemVer for app releases, but keep compatibility decisions separated: Room migrations protect local persisted data/settings, JSONL contract changes affect reporting/backend compatibility, and CSV changes affect the user-facing export contract. CSV or JSONL changes do not require a Room migration unless the stored local data shape also changes.

Release preparation notes live in docs/release-process.md. Current release notes live in RELEASE_NOTES.md.

Terminology

Use these nouns consistently in user-facing text, code comments, docs, and agent work:

  • Coverage sample: one accepted measurement record produced by the scanner.
  • Recorded coverage data: the local collection of coverage samples stored on this device.
  • Local database: the Room/SQLite store. This is the app's source of truth.
  • CSV export: the user-facing export artifact generated from selected database rows.
  • Reporting: sending queued coverage samples according to the reporting setting.
  • JSONL contract: the internal compatibility shape shared with the Node reference scanner. JSONL is not a user-facing export format.
  • Coordinate precision: latitude and longitude are rounded to five decimal places for UI display and CSV export. Stored samples keep their original coordinate values.
  • Scanner states: stopped, running, and error.

Avoid obsolete storage terms such as log, log file, daily file, and daily JSONL when describing app persistence. The app no longer stores coverage data as files.

Reference Project

The existing Node/TypeScript scanner should be linked as a Git submodule at:

external/node-scanner/

That project currently defines the JSONL output shape and has real sample data. Android should mirror the output contract rather than embed the Node runtime.

Useful reference files:

external/node-scanner/src/domain/sample.ts
external/node-scanner/src/domain/cell.ts
external/node-scanner/src/domain/fix.ts
external/node-scanner/src/storage/jsonl.ts
external/node-scanner/data/*.jsonl

Intended Layout

5g-scanner-android/
  assets/icon/          Canonical app icon SVG and generated favicon assets
  app/                  Android application module
  core/                 Plain Kotlin domain models and internal sample encoding
  telemetry/            Android location/connectivity/cellular collectors
  storage/              Database persistence, reporting state, and CSV export
  scripts/              Maintenance scripts, including icon generation
  docs/                 Architecture notes and output contract
  external/
    node-scanner/       Reference Node/TypeScript scanner submodule

Only app/ exists right now. The other modules can be added when the scanner logic starts to grow.

Android Prototype

The current Android app is intentionally small and still uses a single Gradle app module:

settings.gradle.kts
build.gradle.kts
app/build.gradle.kts
app/src/main/AndroidManifest.xml
app/src/main/java/no/politiet/pit/fivegscanner/

It uses a single native Android Activity and avoids Compose. Scanner service work, telemetry sources, storage, reporting, encoding, and domain helpers are split into Kotlin packages inside the app module. AndroidX Room is included for local sample persistence; CSV is the user-facing export format. Android Studio can open the project and sync Gradle.

The Android application id, manifest namespace, and Kotlin base package are all no.politiet.pit.fivegscanner.

The app compiles and targets Android 16/API 36. The minimum supported Android version is Android 10/API 29, matching the first public Android API level with 5G NR telephony classes and the intended modern 5G device fleet.

The emulator build currently uses mock telemetry. It supports:

  • Blocking first-run consent before scanner controls are shown
  • Grant app-level consent
  • Automatic mock scanning after consent through a foreground scanner service with mock radio and mock GNSS sources
  • Accepted samples persisted in a local Room/SQLite database with upload bookkeeping fields and reporting batches
  • Recorded coverage data inspector backed by the database, with CSV exports generated into temporary cache files
  • Single stop/start control on the main scanner screen
  • View sample count, last sample time, and mock radio output
  • Separate settings screen for location mode, reporting, recorded coverage data, and About details
  • RecyclerView-backed Settings, Recorded Coverage Data, and About screens with data-driven row refreshes
  • Scanner state model: stopped, running, or error

The Gradle wrapper is committed so the project can be built consistently from Android Studio or the command line.

Useful verification commands:

.\gradlew.bat assembleDebug
.\gradlew.bat testDebugUnitTest

Before physical-device field testing, run through docs/device-testing-checklist.md.

Icon Workflow

The app icon and favicon files are based on a single square SVG source:

assets/icon/source.svg

It uses a 1024x1024 canvas with grid-aligned geometry so small launcher and favicon outputs stay crisp. Android launcher icons use adaptive icon XML plus the vector foreground in app/src/main/res/drawable/ic_launcher_foreground.xml; no density-specific launcher PNGs are generated because the app targets Android 10+.

Install the small dev toolchain once, then regenerate favicon files with:

npm install
npm run icons

The script uses the pinned @resvg/resvg-js dev dependency for deterministic SVG rasterization. Generated favicon assets are written to assets/icon/generated/.

Internal Reporting Contract

The first target format is append-only JSON Lines:

one CoverageSample JSON object per line

The Android prototype stores accepted samples in Room/SQLite. JSONL is still the internal compatibility contract for reporting and tests, but it is not the user-facing export format.

See docs/output-contract.md for the current contract notes.

Accepted coverage samples are stored in a local Room/SQLite database. The app generates CSV from selected database rows for user-facing export through the Android Sharesheet. JSONL remains internal to reporting/debug workflows.

The prototype now routes mock collection through the same shape planned for real scanning:

RadioTelemetrySource + GnssTelemetrySource
  -> CoverageSampleAssembler
  -> CoverageSampleJsonEncoder
  -> CoverageDatabase

Mock radio/GNSS sources are forced for emulator development, while physical-device fresh installs default to Android telemetry. The Android radio source now registers telephony callbacks on API 31+ and uses the legacy listener fallback on API 29-30, then maps registered LTE/NR serving cells into the existing domain model. Radio events update the latest in-memory snapshot and can request a debounced near-term sample, but accepted coverage samples are still written by the scanner tick after pairing radio with the latest acceptable GNSS fix. The Android GNSS source uses LocationManager, keeps latest reported/usable fixes in memory, and adapts request aggressiveness from low power while stationary to high accuracy while driving.

Multi-SIM collection should happen in the sampler before the UI becomes SIM-aware. On a scanner tick, one acceptable GNSS fix may be paired with every usable active subscription/modem snapshot, producing one coverage sample per active radio source when Android exposes registered radio data. Android subscription IDs and SIM slots are used only inside the Android collector to register callbacks and order sources; they should not be persisted, reported, or exported. The current main UI may continue showing the default-data or first active radio source; a later UI pass should add a small SIM/source indicator and optional swipe left/right behavior for switching the displayed source. The subscription carrier label from Android can be kept for UI/debug display, but the observed network remains the cell MCC/MNC.

The sample model needs a compatibility review before adding more metadata to JSONL. Candidate additions include a compact sensor concept, such as a stable sensorId with sensor/device metadata reported separately, so samples from Android phones, Celerway routers, Waveshare cards, or future collectors can share one contract without repeating bulky device details in every row. Candidate metadata includes device make/model class, OS/platform version, collector app version, radio source count, optional non-sensitive radioSourceIndex, default-data flag, and subscription carrier label. Carrier and SIM metadata should be handled carefully because MCC/MNC already identifies the observed network and display names can be user/device/carrier-specific.

Runtime defaults and tuning values that are natural to adjust during development live in app/src/main/java/no/politiet/pit/fivegscanner/AppConfig.kt. This includes settings defaults, scanner cadence, GNSS quality gates, enhanced privacy precision, reporting intervals/backoff/batch limits, the reporting endpoint and transport mode, and recorded coverage data display/export limits.

Reporting uses a small ReportingTransport boundary. The default endpoint is https://deknings.info/v1/measurements?token=352da26732af23abd4b7ab397a5e8a5311d99bd0954f6654. For local backend testing, use the Developer Reporting endpoint setting in About to point the app at a backend URL reachable from the device, such as http://10.0.2.2:8080/api/coverage-samples from the Android emulator or the host PC's LAN IP from a physical phone. The setting verifies that the value is an HTTP(S) URL and that an OPTIONS request advertises POST support before saving; servers can advertise JSONL support with Accept-Post: application/x-ndjson. Cleartext HTTP is enabled for local development overrides. A mock reporting transport remains available through AppConfig.Reporting.useMockTransport for development. Reporting batches are bounded by sample count, payload size, and a maximum number of batches per drain; stale in_flight batches are marked failed with a recoverable error before the next retry.

Enhanced privacy is applied before a sample is stored. It snaps fix timestamps to UTC midnight, snaps coordinates to the configured grid-cell center, coarsens altitude, removes speed and heading, and replaces precise GNSS quality details with configured privacy-mode values.

Running scanning is owned by ScannerService, a foreground service with a visible notification. MainActivity starts or stops the service based on consent, location and notification permissions, flight-mode/location guards, and the user scanner toggle, then renders the latest in-process service state. This lets scanning continue after the UI leaves the foreground. Automatic scanner restart after phone reboot is intentionally not enabled yet because modern Android treats boot-started location services as a stronger background-location case.

The effective scanner state is derived from app consent, user intent, runtime permissions, device state, and Android service lifecycle:

stateDiagram-v2
    [*] --> ConsentGate
    ConsentGate --> Stopped: Consent missing
    ConsentGate --> PermissionCheck: Consent granted

    Stopped --> PermissionCheck: User starts scanning
    PermissionCheck --> Error: Missing permission / location off / flight mode / no telephony
    PermissionCheck --> Running: All guards pass

    Running --> Error: Temporary guard fails
    Error --> Running: Guard clears and user still wants scanning
    Running --> Stopped: User stops scanning
    Error --> Stopped: User stops scanning

    Running --> CollectTelemetry: Scanner tick or important radio event
    CollectTelemetry --> SkipSample: No radio snapshot or no usable GNSS
    CollectTelemetry --> StoreSample: Radio + acceptable GNSS paired
    StoreSample --> QueueReporting: Coverage sample persisted
    QueueReporting --> Running: Scheduled/manual/continuous reporting owns upload
    SkipSample --> Running: Wait for next tick

    Stopped --> [*]: Service stopped / notification removed
Loading

The About screen includes a small Developer section with a Mock telemetry toggle. Physical-device fresh installs default to Android telemetry. Emulators always force mock telemetry even if the saved toggle is off, so emulator development keeps using simulator-friendly radio/GNSS samples. Turning mock telemetry on for development routes through mock source classes. The main scanner screen shows a subtle MOCK badge in the serving-cell line whenever mock telemetry is active.

The same Developer section includes a compact telemetry diagnostics row for field testing. The row summarizes whether mock or Android telemetry is active, whether sources are running or waiting, and the most useful current clue: recent radio/GNSS/sample ages or the latest sample skip reason. Tapping the row opens a copyable diagnostics dialog with source type, radio source count, radio/GNSS update ages, current GNSS request tier when exposed by the source, last sample attempt age, and latest sample skip reason.

Location Mode and Quality

Location mode controls the starting aggressiveness for Android GNSS/location requests, and movement speed can adapt the active request tier: low power while stationary, balanced while moving slowly, and high accuracy while driving. It does not lower the quality bar for recorded coverage samples.

Balanced
  Default. Saves battery when still or moving slowly, then asks for fresher fixes while driving.

High accuracy
  Best for active mapping and driving. Uses more battery.

Low power
  Uses fewer location updates. Some driving samples may be skipped.

Samples are written only when the paired location is good enough. The current code-configured quality gate is:

horizontal accuracy <= 50 m, when Android provides accuracy
HDOP <= 4.0
GNSS snapshot age <= 30 s
fix age <= 30 s when speed is under 2 m/s
fix age <= 10 s when speed is at least 2 m/s, or speed is unknown
fix age <= 5 s when speed is at least 10 m/s

This means the app should save battery by collecting fewer usable samples, not by accepting stale or imprecise locations. The emulator mock intentionally emits a bad location every sixth sample so this rejection path can be exercised without a physical phone.

The scanner keeps the newest reported GNSS fix separate from the newest usable GNSS fix. If Android reports a newer but worse location, the scanner can continue pairing radio samples with the previous usable fix until that fix ages out. The main UI shows the usable location state: GNSS displays accuracy and age while usable, and switches to reasons such as Fix too old, Too imprecise, or Weak fix when location is no longer useful for mapping.

Product and Privacy Notes

Planning docs:

docs/product-notes.md
docs/privacy-consent.md
docs/settings-model.md
docs/architecture-notes.md

Core first-version assumptions:

  • Scanning requires app-level consent and Android permission grants
  • The normal experience is consent once, then let scanning run in the background
  • Participation can pause by stopping scanning and can stop entirely by uninstalling the app
  • The main UI should expose a clear scanning on/off control for off hours, battery saving, or user choice
  • The prototype samples on a fixed internal cadence; settings should avoid scanner controls that do not change real collection behavior
  • Settings should include location behavior, reporting, recorded coverage data, and About details
  • The local database is the source of truth; CSV is the user-facing export format, while JSONL remains internal to reporting/debug workflows

Notes

Android cellular APIs expose different fields depending on Android version, phone model, carrier, SIM state, and permissions. The app should tolerate missing signal metrics and partial cell identity data.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages