A native Android rewrite of the Expo/React Native ruview-lite app, same real
WiFi RSSI presence-sensing logic, no simulation. This exists to get one
concrete thing the RN version couldn't give reliably: sensing that survives
the app being backgrounded, via a proper foreground Service.
react-native-wifi-reborn was already calling the exact same OS API this app
calls — WifiManager.getConnectionInfo().getRssi(). Going native does not
unlock deeper RSSI access or CSI-level data; that ceiling (per-subcarrier
phase/amplitude) is an Android OS restriction that applies identically to
native and RN apps alike, and still requires rooted monitor-mode firmware or
dedicated hardware (ESP32-S3) to cross.
What native does buy:
- A real foreground
Servicewith a persistent notification, so OEM background-execution limits (the throttling the RN README called out) don't silently stop the sensing loop. - No Metro/dev-client build step, direct Gradle/Android Studio tooling.
- Slightly less overhead per tick (no JS bridge) — irrelevant at 1Hz, but free.
MainActivity.kt permission flow, starts+binds SensingService,
hosts Compose UI
SensingService.kt foreground Service: polls WifiManager every
1s, owns SensorState, exposes StateFlows
(reading, history, baselineMean, ssid,
connectionError), shows status notification
engine/WifiSensingEngine.kt pure Kotlin port of the RN engine — same
constants (60-sample history, 15-sample
baseline, 6-sample window, 2.5x sensitivity,
4dB mean-shift), same math, no Android deps
ui/MainViewModel.kt collects the bound service's StateFlows into
one UiState for Compose to read
ui/RuViewApp.kt root composable: header + 3-tab bar
ui/screens/*.kt Dashboard / Signal / Settings, ported 1:1
from the RN screens, same dark palette
ui/theme/Theme.kt same colors as the RN app's StyleSheets
Data flow: SensingService is the single source of truth (it keeps running
whether or not an Activity is bound to it). MainActivity binds to it after
permission is granted, MainViewModel.bind() starts collecting its flows,
and Compose screens read one UiState — no screen touches the service
directly.
SensingService polls WifiManager.connectionInfo.rssi every second, same
cadence as the RN app — it does not use
ConnectivityManager.NetworkCallback.onCapabilitiesChanged() for
event-driven signal updates, even though that API exists and would in theory
avoid unnecessary polling. Callback cadence and transportInfo availability
for WiFi vary enough across OEMs and API levels that plain polling is the
more predictable choice for a first native port. Worth revisiting once this
is running on real target devices.
The RN SignalScreen.js labeled history[0].rssi (the oldest sample in the
rolling 60-sample window) as "Baseline mean" — after the window filled past
60 seconds that number drifted and was no longer the actual calibrated
baseline. This port exposes the real SensorState.baselineMean via its own
StateFlow and displays that instead.
This project has no committed Gradle wrapper jar (binary file, not something to hand-write). Two ways to build:
Android Studio (recommended) — Open the RuViewNative/ folder directly;
Studio detects the Gradle project, regenerates the wrapper, and syncs
dependencies automatically. Run on a device or emulator with Play Services
(needed for real WiFi radio state — most emulators fake a static AP).
Command line — requires a system-installed Gradle matching
gradle/wrapper/gradle-wrapper.properties (8.7):
gradle wrapper # generates gradlew + the wrapper jar once
./gradlew installDebug # builds and installs on a connected deviceOn first launch, grant the location permission when prompted — same reason as the RN app: Android ties WiFi signal-strength access to it, and the app doesn't read or store actual location.
- Android only (same OS restriction, not a native-vs-RN difference).
- Requires an active WiFi connection.
- Detects that something changed, not what — no person count, no location within the room, no vitals, no pose.
- Sensitivity is a blunt threshold, not a trained model.
- Baseline doesn't auto-invalidate on a network switch — only the manual
"Recalibrate" button resets it. Worth adding a
WifiManagerSSID-change check if you hit this in practice.
Same as before: real presence + vitals + pose needs actual CSI hardware — an ESP32-S3 node running CSI-capture firmware, feeding a signal-processing/ML pipeline this app would subscribe to over MQTT/WebSocket. That's a separate build, not something either the RN or this native app can get to on phone WiFi hardware alone.