Skip to content

Latest commit

 

History

History
266 lines (228 loc) · 12.1 KB

File metadata and controls

266 lines (228 loc) · 12.1 KB

RFC: Android Kotlin → Rust/Tauri Migration

Problem

The android/ directory contains 7 Kotlin files + 3 *.gradle.kts files, all banned by the Hyperpolymath language policy:

android/settings.gradle.kts
android/build.gradle.kts
android/app/build.gradle.kts
android/app/src/main/java/ai/neurophone/MainActivity.kt
android/app/src/main/java/ai/neurophone/NeurophoneService.kt
android/app/src/main/java/ai/neurophone/BootReceiver.kt
android/app/src/main/java/ai/neurophone/NativeLib.kt
android/app/src/main/java/ai/neurophone/widget/NeurophoneAppWidget.kt
android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetActions.kt
android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetConfigureActivity.kt

This keeps two estate CI gates permanently red:

  • Check for Banned Languages (workflow language-policy.yml, step Check for Java/Kotlin files)

  • governance / Language / package anti-pattern policy

Both are sourced from .github/workflows/language-policy.yml:87-94 which hard-fails on any .kt / .kts / *.java match.

Goal

  • Migrate the Android UI + service to Tauri 2.0 (Rust backend + AffineScript/web UI).

  • Both CI checks turn green.

  • Behaviour preserved: sensor capture, foreground presence, widgets, boot-start.

  • Sub-PRs are small and reviewable; nothing bulk-deleted until owner approves this RFC.

Component inventory + replacement mapping

Kotlin component LoC Current role Replacement

MainActivity.kt

377

UI shell (status / input / response), sensor capture in-activity, permissions, intent handling (MAIN, SEND, VIEW neurophone://, ASSIST, widget query).

Tauri 2 activity (gen/android/-scaffolded MainActivity extends TauriActivity). UI markup authored in AffineScript compiling to Deno-ESM, loaded by Tauri’s webview. Rust commands expose init/start/stop/query/get_neural_context via #[tauri::command]. Sensor capture moves out of activity → service.

NeurophoneService.kt

162

Service subclass: wake-lock, accelerometer @ 50 Hz, salience window, Notification.Builder foreground channel, 1 Hz widget tick, calls NativeLib.init/start/stop/pushSensorEvent/getNeuralContext.

Rust foreground service via thin Java entry-point. Service class NeurophoneRuntimeService extends Service is auto-generated at build time under gen/android/app/src/main/java/ (Tauri convention — generated Java, not hand-written Kotlin). Lifecycle hooks (onCreate / onStartCommand / onDestroy / onSensorChanged) immediately JNI_OnLoad into Rust fns in crates/neurophone-android. Salience window, sensor processing, wake-lock release, and widget publishing all in Rust.

BootReceiver.kt

25

BroadcastReceiver for BOOT_COMPLETED + LOCKED_BOOT_COMPLETED; starts service if widget was running pre-reboot.

Generated Java BootReceiver extends BroadcastReceiver under gen/android/. Single onReceive body: JNI into Rust crates/neurophone-android::boot_receive.

NativeLib.kt

93

JNI surface declarations + sensor-type-string→int mapping.

DELETE entirely. All JNI exports already live (as stubs) in crates/neurophone-android/src/lib.rs. Sensor mapping moves to Rust.

NeurophoneAppWidget.kt

152

AppWidgetProvider (home-screen widget); renders title/state/salience progress bar/Ask button via RemoteViews; persists widget state to SharedPreferences.

Generated Java NeurophoneAppWidget extends AppWidgetProvider under gen/android/. onUpdate / onReceive each contain ONE JNI call into Rust. RemoteViews construction stays in Java (it has to — RemoteViews cannot be assembled from JNI without round-tripping every setter), but the decision of what to render (running flag, salience value, description string) comes from Rust. Prefs storage moves to Rust via ndk-context-mediated JNI calls or a Rust-side SQLite cache.

NeurophoneWidgetActions.kt

47

Lightweight BroadcastReceiver dispatching PUBLISH_STATE / FORCE_REFRESH from non-widget callers.

Generated Java NeurophoneWidgetActions extends BroadcastReceiver; body JNIs into Rust dispatch fn.

NeurophoneWidgetConfigureActivity.kt

70

Widget config UI (one checkbox: local-only mode), saved to prefs.

Generated Java NeurophoneWidgetConfigureActivity extends Activity with programmatic layout. Saving local-only-mode goes through JNI to Rust config storage. Acceptable alternative: drop the widget config entirely — a single setting like "local-only" can move to the main Tauri UI and a sensible default applied at widget install.

*.gradle.kts (×3)

~100

Project + app module build config.

Replaced by Tauri-generated gen/android//.gradle.kts* at cargo tauri android init time. These are platform-prescribed by Android Gradle Plugin: there is no Rust replacement and Tauri vends them. The CI exemption section below covers this.

The unavoidable JVM-bytecode surface

Android instantiates the following by class name at platform boundaries. They cannot be implemented in pure Rust because Android’s class loader demands a class implementing the listed Android framework superclass:

  • Activity (main + widget-configure)

  • Service

  • BroadcastReceiver (boot + widget actions + widget provider)

Three resolution paths exist; this RFC proposes (A) and explicitly rejects (B) and ©. Owner picks before sub-PR work begins.

Path (A) PROPOSED — Tauri-generated Java shims under gen/android/

  • cargo tauri android init generates per-platform Java glue inside src-tauri/gen/android/ (analogous to how node_modules/ is generated but not authored).

  • These files are not Kotlin, they are minimal Java each delegating immediately to a single JNI function in Rust.

  • Add gen/android/ to the Check for Java/Kotlin files step’s exemption list — same exemption pattern as node_modules/ for the Check for ReScript files step.

  • The governance / Language / package anti-pattern policy check is estate-wide (hyperpolymath/standards); adding the exemption needs a one-line change in the reusable workflow caller. Pre-coordination required.

  • Outcome: zero hand-written Kotlin/Java in the repo’s source tree; CI passes; behaviour preserved.

Path (B) REJECTED — drop the widget surface

  • Replace home-screen widget with a quick-settings tile + persistent notification. Both can be declared in AndroidManifest.xml and driven from a service whose only Java shim is auto-generated.

  • Pro: smaller JVM surface.

  • Con: regresses a shipped, user-visible feature; the OS integration doc (docs/OS_INTEGRATION.adoc) lists the home-screen widget as the primary integration point.

Path © REJECTED — JVM bytecode generation from Rust

  • Use j4rs / dx-java / manual .class emission to ship JVM classes built from Rust at compile time.

  • Pro: no Java source at all.

  • Con: experimental, fragile, no maintained tooling for AppWidgetProvider superclass binding; not worth the risk.

Sub-PR sequence

Each is a small, reviewable PR. Numbers are PR ordering, not commit count.

  1. #1 (THIS PR — DRAFT, no code changes) — RFC for owner review. No Kotlin deleted, no Tauri scaffolding generated.

  2. #2 — CI exemption for generated mobile shims. Update .github/workflows/language-policy.yml step Check for Java/Kotlin files to exempt src-tauri/gen/android/. Coordinated with hyperpolymath/standards for the reusable governance check. Lands before any Tauri scaffolding so CI doesn’t go redder en route.

  3. #3 — Tauri scaffolding (no behaviour). Run cargo tauri android init; commit src-tauri/ + generated gen/android/. No deletion of the existing android/ Gradle project; both exist side by side so we can A/B sanity-check.

  4. #4 — Port NativeLib to Rust. Fill out crates/neurophone-android/src/lib.rs with the 10 JNI exports the current Kotlin NativeLib declares. Wire to existing crates (neurophone-core, sensors, lsm, esn, bridge, llm, claude-client). Tests: JNI roundtrip via a Rust test that simulates JNIEnv.

  5. #5 — Port NeurophoneService to Rust. Java shim under gen/android/ delegates onCreate / onStartCommand / onDestroy / onSensorChanged to Rust. Salience window, wake-lock plumbing, widget tick — all Rust.

  6. #6 — Port BootReceiver to Rust. Same shim pattern, single fn.

  7. #7 — Port widget triple (provider / actions / configure) to Rust. Java shims under gen/android/ are minimal; widget render logic (selecting strings, salience-pct, what RemoteViews setters to call) is decided in Rust and pushed back to the shim via a single data-bearing JNI fn.

  8. 8 — AffineScript UI (replaces activity_main.xml view binding). Tauri loads the AffineScript-compiled UI in webview; [tauri::command] wires start/stop/query/get_context to the Rust core.

  9. #9 — Delete legacy android/ tree. Both banned-language checks now green. Behaviour parity verified by running APK on hardware + Termux CLI smoke test.

PRs #2 and #3 are sequenced and blocking. PRs #4 through #7 can land in parallel after #3. PR #8 needs #4 done. PR #9 is gated on all prior.

Open questions for owner

  1. Tauri vs Dioxus. Policy lists both. RFC recommends Tauri 2 for reasons: (a) gen/android/ shim pattern is documented and tested, (b) AffineScript→ESM→webview is the cleanest UI fit, (c) Dioxus mobile on Android (cargo-mobile2) is newer and has thinner foreground-service precedent. Confirm or override.

  2. CI exemption scope. OK to exempt src-tauri/gen/android/ from the Java/Kotlin check? This is the linchpin — without this exemption the migration cannot land cleanly because Android demands JVM bytecode at three system boundaries (Service / BroadcastReceiver / AppWidgetProvider).

  3. Estate governance check. Who owns the governance / Language / package anti-pattern policy callable in hyperpolymath/standards? Cross-repo PR needed before #2.

  4. Widget config sacrifice. Acceptable to drop the widget-configure activity (Path B partial) in exchange for a smaller JVM surface? Saves one shim.

  5. Behaviour-parity verification. Hardware test plan: Oppo Reno 13 (current target per app/build.gradle.kts:21)? Or use Android emulator + Termux on a generic device?

  6. SPDX header policy clash. Existing Kotlin + AndroidManifest.xml use PMPL-1.0-or-later; in-repo hooks/validate-spdx.sh matches PMPL; but the local .git/hooks/pre-commit still expects MPL-2.0. This RFC uses MPL-2.0 to land cleanly under the active local hook, but new sub-PRs will need a resolved policy. Recommend updating .git/hooks/pre-commit to recognize neurophone as a PMPL repo and re-stamping the existing PMPL files if MPL is canonical, or the reverse if PMPL is canonical.

Acceptance criteria

  • Check for Banned Languages job — green on main.

  • governance / Language / package anti-pattern policy — green on main.

  • android/ directory — removed.

  • APK builds via cargo tauri android build.

  • Foreground service + boot-restart + widget + sensor capture all work on hardware.

  • Termux CLI smoke test passes (neurophone command — unchanged by this migration, but verified not regressed).

Non-goals

  • No changes to the seven core Rust crates (lsm, esn, bridge, sensors, llm, claude-client, neurophone-core) outside what JNI wiring strictly requires.

  • No changes to the Termux CLI path.

  • No new dependencies beyond tauri, tauri-build, and the android-activity / ndk ecosystem crates Tauri already pulls in.