diff --git a/MOBILE.adoc b/MOBILE.adoc new file mode 100644 index 0000000..95fa372 --- /dev/null +++ b/MOBILE.adoc @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: CC-BY-SA-4.0 += Mobile target + +The mobile client MUST use *Tauri 2.0+* or *Dioxus* (Rust-first) per the estate +language policy. Kotlin/Gradle/Swift are banned for mobile. + +The previous Kotlin/Gradle Android client (`android/`) was removed 2026-06-24 as a +policy violation (recoverable from git history). Rebuild the mobile target in +Tauri/Dioxus. Note: the Rust core (NativeLib JNI surface) is reusable via Tauri. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts deleted file mode 100644 index cbe93e8..0000000 --- a/android/app/build.gradle.kts +++ /dev/null @@ -1,81 +0,0 @@ -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") -} - -android { - namespace = "ai.neurophone" - compileSdk = 34 - - defaultConfig { - applicationId = "ai.neurophone" - minSdk = 26 - targetSdk = 34 - versionCode = 1 - versionName = "1.0.0" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - ndk { - // Supported ABIs - arm64-v8a for Oppo Reno 13 (Dimensity 8350) - abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") - } - - externalNativeBuild { - cmake { - cppFlags += "" - } - } - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - buildFeatures { - viewBinding = true - } - - // Native library location - sourceSets { - getByName("main") { - jniLibs.srcDirs("src/main/jniLibs") - } - } -} - -dependencies { - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.11.0") - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - - // Coroutines for async operations - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") - - // JSON parsing - implementation("org.json:json:20231013") - - // Lifecycle - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") - - // Testing - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") -} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 111154c..0000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/assets/gossamer-ui/README.adoc b/android/app/src/main/assets/gossamer-ui/README.adoc deleted file mode 100644 index ab4d341..0000000 --- a/android/app/src/main/assets/gossamer-ui/README.adoc +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -= Gossamer webview UI (AffineScript) -:toc: - -NeuroSymbolic Phone user interface, reimplemented in *AffineScript* and loaded by -the Gossamer Android shell's webview. This is sub-PR #8 of the Android -Kotlin→Rust/Gossamer migration (epic #83, RFC PR #97, sub-issue #114). - -It replaces the Kotlin UI that lived in: - -* `android/app/src/main/java/ai/neurophone/MainActivity.kt` (view/controller) -* `android/app/src/main/res/layout/activity_main.xml` (layout) - -The Kotlin sources are left in place for now; this PR only adds the replacement -web UI + build wiring. Removal of the Kotlin UI happens on the migration's cutover. - -== Layout - -[source] ----- -gossamer-ui/ - index.html webview entry; element ids match ui.affine queries - styles.css visual parity with activity_main.xml - deno.json Deno build/check/fmt tasks (no npm/Node) - build.sh AffineScript -> Deno-ESM build wiring - src/ - bridge.affine typed bridge to the native Rust core (was NativeLib JNI) - ui.affine view/controller logic (was MainActivity) - dist/ - ui.mjs GENERATED-OUTPUT STUB (see "Build wiring") ----- - -== What the UI does (ported 1:1 from MainActivity) - -* On load: `bridge.init(defaultConfig)` — config ported verbatim from - `MainActivity.createConfig()` (loop 20ms; sensor 50Hz/buf100/dim32; - LSM 8×8×8 / ρ0.9; ESN 300 / ρ0.95). -* Start/Stop button toggles the neural loop (`bridge.start` / `bridge.stop`). -* While running: polls `bridge.getNeuralContext()` every 500ms and renders it. -* Query box + Send: `bridge.query(message, preferLocal)`; "Prefer Local" toggle - routes smart/local/cloud. `queryLocal` / `queryClaude` / `getState` are also - exposed on the bridge for parity with the JNI surface. - -Sensor *acquisition* is intentionally NOT in this UI: under the migration the -foreground Rust service (sub-PR #4) registers Android sensors natively and feeds -the core. `bridge.processSensor` remains exposed for parity / manual injection. - -== Bridge assumptions (TODO(#83 rebase)) - -The native bridge contract is owned by parallel, *not-yet-merged* sub-PRs: - -* *#3* — Gossamer scaffolding / webview host (how assets load; what host object - is injected). -* *#4* — NativeLib → Rust JNI (the native command implementations). - -Until those merge, this UI assumes: - -. Gossamer injects a global host object `globalThis.gossamer` into the webview. -. `gossamer.invoke(cmd: string, args: object): Promise` does one typed, - async IPC round-trip to the Rust core and resolves with its string/JSON reply. -. Command names are the snake_case forms of the old `NativeLib` methods: - `init`, `start`, `stop`, `process_sensor`, `query`, `query_local`, - `query_claude`, `get_neural_context`, `get_state`, `reset`, `is_running`. -. The host serves `index.html` as the webview root and runs module scripts after - injecting the bridge. - -Every such assumption is also marked inline with `TODO(#83 rebase)` / -`TODO(#83)`. Reconcile against #3/#4 on rebase. - -== Build wiring - -AffineScript (`.affine`) compiles to Deno-ESM via the estate `affinescript` -toolchain (OCaml/Dune CLI: `check` / `fmt` / `compile --target deno-esm`). Driven -through Deno tasks (no npm/Node): - -[source,sh] ----- -deno task check:ui # affine/ownership + type check (bridge.affine, ui.affine) -deno task build:ui # compile src/ui.affine -> dist/ui.mjs -deno task fmt:ui # format sources ----- - -`build.sh` is the single entry point and is CI-ready. - -=== The dist/ui.mjs stub - -The `affinescript` compiler is *not yet vendored* in this repo or CI. So -`dist/ui.mjs` is currently a hand-written *generated-output stub* that faithfully -mirrors `src/ui.affine` + `src/bridge.affine`, so the webview is loadable and -reviewable today. `build.sh` falls back to the committed stub when the compiler -is absent (and says so loudly). TODO(#83): vendor/pin the toolchain, flip -`USE_STUB=0` in `build.sh`, regenerate `dist/ui.mjs`, delete the stub, and wire -`deno task build:ui` into CI. - -== Language policy - -AffineScript (primary app language) + HTML/CSS + a Deno-run Bash build script. -No TypeScript, Node, or npm. The single `.mjs` is a *compiler-output stub*, not -hand-authored application logic — it is replaced by real compiled output once the -toolchain lands. diff --git a/android/app/src/main/assets/gossamer-ui/build.sh b/android/app/src/main/assets/gossamer-ui/build.sh deleted file mode 100755 index 249015b..0000000 --- a/android/app/src/main/assets/gossamer-ui/build.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: MPL-2.0 -# -# Build wiring: compile the AffineScript Gossamer UI (src/*.affine) to a -# Deno-ESM module (dist/ui.mjs) that the Android webview loads via index.html. -# -# Pipeline (per the AffineScript toolchain — OCaml/Dune `affinescript` CLI): -# affinescript check src/*.affine # affine/ownership + type check -# affinescript fmt src/*.affine # formatting (with --fmt) -# affinescript compile --target deno-esm -o dist/ui.mjs src/ui.affine -# -# TODO(#83 rebase): the `affinescript` compiler is NOT yet vendored in this repo -# or CI. Until it is, this script DOES NOT regenerate dist/ui.mjs; the -# committed hand-written stub (dist/ui.mjs) stands in. When the toolchain -# lands: drop the stub, flip USE_STUB=0, and wire `deno task build:ui` into CI. -# -# Usage: -# bash build.sh # build dist/ui.mjs from src/*.affine -# bash build.sh --check-only # type/ownership check only -# bash build.sh --fmt # format src/*.affine in place -set -euo pipefail - -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SRC_DIR="$HERE/src" -DIST_DIR="$HERE/dist" -OUT="$DIST_DIR/ui.mjs" -ENTRY="$SRC_DIR/ui.affine" - -# TODO(#83): set to 0 once the affinescript compiler is available in CI/dev. -USE_STUB=1 - -mode="build" -case "${1:-}" in - --check-only) mode="check" ;; - --fmt) mode="fmt" ;; - "") mode="build" ;; - *) echo "unknown arg: $1" >&2; exit 2 ;; -esac - -if ! command -v affinescript >/dev/null 2>&1; then - echo "[build.sh] 'affinescript' compiler not found on PATH." - echo "[build.sh] TODO(#83): vendor/pin the AffineScript toolchain (OCaml/Dune)." - if [ "$USE_STUB" = "1" ]; then - echo "[build.sh] Using committed stub: $OUT (no regeneration)." - [ -f "$OUT" ] || { echo "[build.sh] ERROR: stub $OUT missing." >&2; exit 1; } - exit 0 - fi - exit 1 -fi - -case "$mode" in - check) - affinescript check "$SRC_DIR"/*.affine - ;; - fmt) - affinescript fmt "$SRC_DIR"/*.affine - ;; - build) - affinescript check "$SRC_DIR"/*.affine - mkdir -p "$DIST_DIR" - affinescript compile --target deno-esm -o "$OUT" "$ENTRY" - echo "[build.sh] Wrote $OUT" - ;; -esac diff --git a/android/app/src/main/assets/gossamer-ui/deno.json b/android/app/src/main/assets/gossamer-ui/deno.json deleted file mode 100644 index cc7d7bb..0000000 --- a/android/app/src/main/assets/gossamer-ui/deno.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "//": "SPDX-License-Identifier: MPL-2.0", - "//license": "deno.json carries SPDX via this key; JSON has no comment syntax.", - "//purpose": "Deno-based build/check tasks for the AffineScript Gossamer UI. No npm/Node — Deno only, per estate language policy.", - "tasks": { - "build:ui": "bash build.sh", - "check:ui": "bash build.sh --check-only", - "fmt:ui": "bash build.sh --fmt" - }, - "fmt": { - "include": ["dist/", "index.html", "styles.css"], - "exclude": ["dist/ui.mjs"] - }, - "lint": { - "include": ["dist/"] - }, - "//toolchain": "AffineScript compiler (OCaml/Dune) is invoked by build.sh, not by Deno. See README.adoc. TODO(#83): vendor/pin the affinescript compiler and wire into CI." -} diff --git a/android/app/src/main/assets/gossamer-ui/dist/ui.mjs b/android/app/src/main/assets/gossamer-ui/dist/ui.mjs deleted file mode 100644 index b3264d3..0000000 --- a/android/app/src/main/assets/gossamer-ui/dist/ui.mjs +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// -// GENERATED-OUTPUT STUB — hand-written stand-in for the Deno-ESM that the -// AffineScript compiler will emit from src/ui.affine + src/bridge.affine. -// -// Why this exists: the AffineScript toolchain (OCaml/Dune `affinescript` -// compiler, see README.adoc "Build wiring") is not yet vendored in this repo or -// CI. Committing a faithful stub keeps the Gossamer webview loadable and -// reviewable today. `build.sh` overwrites this file with real compiled output -// once the toolchain is available. -// -// TODO(#83 rebase): delete this stub once `deno task build:ui` (or CI) produces -// dist/ui.mjs from the .affine sources. Keep behaviour in sync with ui.affine -// until then. -// -// This stub mirrors src/ui.affine 1:1 and the bridge contract in -// src/bridge.affine. It assumes the Gossamer host injects `globalThis.gossamer` -// with `invoke(cmd, args) => Promise`. - -const host = globalThis.gossamer ?? { - // Fallback shim so the page is inspectable in a plain browser during dev. - // TODO(#83): remove once the real host is always present. - invoke: async (cmd) => { - console.warn(`[gossamer-ui] no host bridge; stub invoke('${cmd}')`); - return cmd === "get_neural_context" ? "[stub] no native bridge" : ""; - }, -}; - -// --- bridge.affine port --------------------------------------------------- -const defaultConfig = { - loop_interval_ms: 20, - debug: false, - sensor: { sample_rate_hz: 50.0, buffer_size: 100, output_dim: 32 }, - lsm: { dimensions: [8, 8, 8], spectral_radius: 0.9 }, - esn: { reservoir_size: 300, spectral_radius: 0.95 }, -}; - -const bridge = { - init: async (config) => (await host.invoke("init", { configJson: JSON.stringify(config) })) === "true", - start: async () => (await host.invoke("start", {})) === "true", - stop: async () => { await host.invoke("stop", {}); }, - query: (message, preferLocal) => host.invoke("query", { message, preferLocal }), - queryLocal: (message) => host.invoke("query_local", { message }), - queryClaude: (message) => host.invoke("query_claude", { message }), - getNeuralContext: () => host.invoke("get_neural_context", {}), - getState: () => host.invoke("get_state", {}), -}; - -// --- ui.affine port ------------------------------------------------------- -const $ = (id) => document.getElementById(id); -const el = { - statusText: $("statusText"), - neuralContextText: $("neuralContextText"), - inputField: $("inputField"), - responseText: $("responseText"), - startButton: $("startButton"), - sendButton: $("sendButton"), - localSwitch: $("localSwitch"), - activityIndicator: $("activityIndicator"), -}; - -let isSystemRunning = false; -let contextTimer = null; - -function updateUI() { - el.startButton.textContent = isSystemRunning ? "Stop" : "Start"; - el.sendButton.disabled = !isSystemRunning; - el.inputField.disabled = !isSystemRunning; -} - -async function pollContextOnce() { - if (!isSystemRunning) return; - try { - el.neuralContextText.textContent = await bridge.getNeuralContext(); - } catch (_e) { - // non-fatal; keep polling - } - contextTimer = setTimeout(pollContextOnce, 500); -} - -function startContextUpdates() { contextTimer = setTimeout(pollContextOnce, 500); } -function stopContextUpdates() { if (contextTimer != null) { clearTimeout(contextTimer); contextTimer = null; } } - -async function startSystem() { - if (await bridge.start()) { - isSystemRunning = true; - startContextUpdates(); - updateUI(); - el.statusText.textContent = "System running"; - } else { - el.statusText.textContent = "Failed to start"; - } -} - -async function stopSystem() { - await bridge.stop(); - isSystemRunning = false; - stopContextUpdates(); - updateUI(); - el.statusText.textContent = "System stopped"; -} - -const toggleSystem = () => (isSystemRunning ? stopSystem() : startSystem()); - -async function sendQuery() { - const message = el.inputField.value.trim(); - if (message === "") { el.responseText.textContent = "Please enter a message"; return; } - el.activityIndicator.hidden = false; - el.sendButton.disabled = true; - try { - const response = await bridge.query(message, el.localSwitch.checked); - el.responseText.textContent = response === "" ? "No response received" : response; - } catch (e) { - el.responseText.textContent = "Error: " + e.message; - } finally { - el.activityIndicator.hidden = true; - el.sendButton.disabled = false; - } -} - -async function initializeSystem() { - el.statusText.textContent = "Initializing..."; - try { - const ok = await bridge.init(defaultConfig); - el.statusText.textContent = ok ? "System initialized" : "Initialization failed"; - } catch (e) { - el.statusText.textContent = "Error: " + e.message; - } -} - -async function main() { - el.startButton.addEventListener("click", toggleSystem); - el.sendButton.addEventListener("click", sendQuery); - updateUI(); - await initializeSystem(); -} - -main(); diff --git a/android/app/src/main/assets/gossamer-ui/index.html b/android/app/src/main/assets/gossamer-ui/index.html deleted file mode 100644 index a7afd14..0000000 --- a/android/app/src/main/assets/gossamer-ui/index.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - NeuroSymbolic Phone - - - -
-

NeuroSymbolic Phone

-

LSM + ESN + Llama 3.2 + Claude

-
- -
- Status: - Initializing... - -
- -

Neural Context:

-
[No neural state available]
- -

Query:

- - -
- - - - -
- -

Response:

-
Response will appear here...
- -
Sensors → LSM → Bridge → LLM ↔ ESN → Output
- - - - - diff --git a/android/app/src/main/assets/gossamer-ui/src/bridge.affine b/android/app/src/main/assets/gossamer-ui/src/bridge.affine deleted file mode 100644 index db5eac0..0000000 --- a/android/app/src/main/assets/gossamer-ui/src/bridge.affine +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// face: JaffaScript -// -// Typed bridge from the Gossamer webview UI to the native Rust core. -// -// This module mirrors the JNI surface previously exposed to the Kotlin -// `MainActivity` via `ai.neurophone.NativeLib` (see -// android/app/src/main/java/ai/neurophone/NativeLib.kt). In the -// Kotlin→Rust/Gossamer migration the native functions are reached through the -// Gossamer webview's JS↔native FFI bridge rather than Android JNI directly. -// -// TODO(#83 rebase): The concrete bridge object name and call convention come -// from sub-PR #3 (Gossamer scaffolding / webview host) and #4 (NativeLib→Rust -// JNI). Those are in parallel and NOT merged yet. The assumptions encoded -// below are documented in README.adoc and must be reconciled on rebase. -// -// Assumed contract (see README.adoc "Bridge assumptions"): -// - Gossamer injects a global `gossamer` host object into the webview. -// - `gossamer.invoke(cmd: String, args: Record): Promise` performs a -// typed, async IPC round-trip to the Rust core and resolves with the -// JSON / string payload the native side returns. -// - Command names match the snake_case JNI method names from NativeLib. - -// --- Affine note --------------------------------------------------------- -// The Gossamer host handle is an affine resource: it is borrowed for the -// duration of a single `invoke` and must not be aliased across awaits. We -// model it as a borrow (`&host`) rather than an owned move so the UI layer -// keeps a single long-lived reference for the app lifetime. - -type NeuralConfig = { - loop_interval_ms: Int, - debug: Bool, - sensor: { sample_rate_hz: Float, buffer_size: Int, output_dim: Int }, - lsm: { dimensions: List, spectral_radius: Float }, - esn: { reservoir_size: Int, spectral_radius: Float }, -} - -// The host object Gossamer injects. Marked `extern` because it is provided by -// the Idris2/Zig webview host, not by this module. -extern let host: { invoke: (String, Record) -> Promise } - -// Default config, ported verbatim from MainActivity.createConfig(). -let defaultConfig: NeuralConfig = { - loop_interval_ms: 20, - debug: false, - sensor: { sample_rate_hz: 50.0, buffer_size: 100, output_dim: 32 }, - lsm: { dimensions: [8, 8, 8], spectral_radius: 0.9 }, - esn: { reservoir_size: 300, spectral_radius: 0.95 }, -} - -// init(configJson?) -> Bool -pub fn init(config: NeuralConfig): Promise = { - let json = Json.stringify(config) - let out = await host.invoke("init", { configJson: json }) - return out == "true" -} - -// start() -> Bool -pub fn start(): Promise = { - let out = await host.invoke("start", {}) - return out == "true" -} - -// stop() -pub fn stop(): Promise = { - let _ = await host.invoke("stop", {}) - return unit -} - -// processSensor(sensorType, values, timestamp, accuracy) -> Bool -pub fn processSensor( - sensorType: Int, - values: List, - timestamp: Int, - accuracy: Int, -): Promise = { - let out = await host.invoke("process_sensor", { - sensorType: sensorType, - values: values, - timestamp: timestamp, - accuracy: accuracy, - }) - return out == "true" -} - -// query(message, preferLocal) -> String (smart auto-select) -pub fn query(message: String, preferLocal: Bool): Promise = - host.invoke("query", { message: message, preferLocal: preferLocal }) - -// queryLocal(message) -> String (Llama 3.2) -pub fn queryLocal(message: String): Promise = - host.invoke("query_local", { message: message }) - -// queryClaude(message) -> String (cloud) -pub fn queryClaude(message: String): Promise = - host.invoke("query_claude", { message: message }) - -// getNeuralContext() -> String (formatted) -pub fn getNeuralContext(): Promise = - host.invoke("get_neural_context", {}) - -// getState() -> String (JSON) -pub fn getState(): Promise = - host.invoke("get_state", {}) - -// reset() -pub fn reset(): Promise = { - let _ = await host.invoke("reset", {}) - return unit -} - -// isRunning() -> Bool -pub fn isRunning(): Promise = { - let out = await host.invoke("is_running", {}) - return out == "true" -} diff --git a/android/app/src/main/assets/gossamer-ui/src/ui.affine b/android/app/src/main/assets/gossamer-ui/src/ui.affine deleted file mode 100644 index a24e456..0000000 --- a/android/app/src/main/assets/gossamer-ui/src/ui.affine +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// face: JaffaScript -// -// AffineScript webview UI for the NeuroSymbolic Phone, loaded by the Gossamer -// Android shell. This reimplements the view/controller logic previously in -// `ai.neurophone.MainActivity` (android/.../MainActivity.kt) and the layout in -// `res/layout/activity_main.xml`. -// -// Responsibilities (ported 1:1 from MainActivity): -// - Initialise the native system on load (bridge.init). -// - Start/Stop toggle for the neural processing loop (bridge.start/stop). -// - Poll and render the neural context every 500ms while running. -// - Send a query (smart / local / claude) and render the response. -// - "Prefer Local" toggle drives query routing. -// -// Sensor acquisition (SensorManager / SensorEventListener in Kotlin) is NOT -// owned by this UI layer anymore: under the migration the foreground Rust -// service (sub-PR #4) registers sensors natively and feeds the core directly. -// The UI only drives the user-facing controls and `processSensor` is exposed on -// the bridge for parity / manual injection. -// -// TODO(#83 rebase): wire the DOM-host binding (`Dom.*` below) to whatever the -// Gossamer webview runtime actually exposes; confirm event-loop / timer APIs. - -import bridge - -// --- DOM host (provided by the webview runtime) -------------------------- -// TODO(#83): These are thin typed wrappers over the document API the Gossamer -// webview exposes. Names assumed; reconcile with #3's host on rebase. -extern module Dom { - fn byId(id: String): Element - fn on(el: Element, event: String, handler: () -> Promise): Unit - fn setText(el: Element, text: String): Unit - fn getValue(el: Element): String - fn setValue(el: Element, v: String): Unit - fn setEnabled(el: Element, enabled: Bool): Unit - fn setVisible(el: Element, visible: Bool): Unit - fn isChecked(el: Element): Bool - fn focus(el: Element): Unit - fn setTimeout(ms: Int, handler: () -> Promise): TimerId - fn clearTimeout(id: TimerId): Unit -} - -// --- UI element handles (affine: each bound once at startup) -------------- -let statusText = Dom.byId("statusText") -let neuralContextText = Dom.byId("neuralContextText") -let inputField = Dom.byId("inputField") -let responseText = Dom.byId("responseText") -let startButton = Dom.byId("startButton") -let sendButton = Dom.byId("sendButton") -let localSwitch = Dom.byId("localSwitch") -let activityIndicator = Dom.byId("activityIndicator") - -// --- Mutable UI state ----------------------------------------------------- -let mut isSystemRunning: Bool = false -let mut contextTimer: Option = None - -// updateUI(): mirrors MainActivity.updateUI() -fn updateUI(): Unit = { - Dom.setText(startButton, if isSystemRunning { "Stop" } else { "Start" }) - Dom.setEnabled(sendButton, isSystemRunning) - Dom.setEnabled(inputField, isSystemRunning) -} - -// initializeSystem(): mirrors MainActivity.initializeSystem() -fn initializeSystem(): Promise = { - Dom.setText(statusText, "Initializing...") - try { - let ok = await bridge.init(bridge.defaultConfig) - Dom.setText(statusText, if ok { "System initialized" } else { "Initialization failed" }) - } catch e { - Dom.setText(statusText, "Error: " + e.message) - } - return unit -} - -// startContextUpdates(): mirrors MainActivity.startContextUpdates() -// Poll getNeuralContext every 500ms. Re-arms itself while running. -fn pollContextOnce(): Promise = { - if !isSystemRunning { return unit } - try { - let ctx = await bridge.getNeuralContext() - Dom.setText(neuralContextText, ctx) - } catch e { - // Non-fatal: keep polling (matches Kotlin's Log.w + continue). - } - contextTimer = Some(Dom.setTimeout(500, pollContextOnce)) - return unit -} - -fn startContextUpdates(): Unit = { - contextTimer = Some(Dom.setTimeout(500, pollContextOnce)) -} - -fn stopContextUpdates(): Unit = { - match contextTimer { - Some(id) => { Dom.clearTimeout(id); contextTimer = None } - None => unit - } -} - -// startSystem(): mirrors MainActivity.startSystem() -fn startSystem(): Promise = { - let ok = await bridge.start() - if ok { - isSystemRunning = true - startContextUpdates() - updateUI() - Dom.setText(statusText, "System running") - } else { - Dom.setText(statusText, "Failed to start") - } - return unit -} - -// stopSystem(): mirrors MainActivity.stopSystem() -fn stopSystem(): Promise = { - await bridge.stop() - isSystemRunning = false - stopContextUpdates() - updateUI() - Dom.setText(statusText, "System stopped") - return unit -} - -// toggleSystem(): mirrors MainActivity.toggleSystem() -fn toggleSystem(): Promise = - if isSystemRunning { stopSystem() } else { startSystem() } - -// sendQuery(): mirrors MainActivity.sendQuery() -fn sendQuery(): Promise = { - let message = String.trim(Dom.getValue(inputField)) - if message == "" { - Dom.setText(responseText, "Please enter a message") - return unit - } - Dom.setVisible(activityIndicator, true) - Dom.setEnabled(sendButton, false) - try { - let preferLocal = Dom.isChecked(localSwitch) - let response = await bridge.query(message, preferLocal) - Dom.setText(responseText, if response == "" { "No response received" } else { response }) - } catch e { - Dom.setText(responseText, "Error: " + e.message) - } finally { - Dom.setVisible(activityIndicator, false) - Dom.setEnabled(sendButton, true) - } - return unit -} - -// main(): wire handlers and boot. Mirrors MainActivity.onCreate(). -pub fn main(): Promise = { - Dom.on(startButton, "click", toggleSystem) - Dom.on(sendButton, "click", sendQuery) - updateUI() - await initializeSystem() - return unit -} diff --git a/android/app/src/main/assets/gossamer-ui/styles.css b/android/app/src/main/assets/gossamer-ui/styles.css deleted file mode 100644 index bf92090..0000000 --- a/android/app/src/main/assets/gossamer-ui/styles.css +++ /dev/null @@ -1,102 +0,0 @@ -/* SPDX-License-Identifier: MPL-2.0 - * - * Styling for the Gossamer webview NeuroSymbolic Phone UI. - * Visual parity with the old Android layout (res/layout/activity_main.xml): - * 16dp page padding, centred header, monospace neural-context panel, etc. - */ - -:root { - --pad: 16px; - --muted: #666; - --faint: #999; - --ok: #4caf50; - --panel: #f5f5f5; - --panel-2: #fafafa; - --text: #333; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - padding: var(--pad); - font-family: system-ui, sans-serif; - display: flex; - flex-direction: column; - min-height: 100vh; - color: var(--text); -} - -header { text-align: center; margin-bottom: 16px; } -header h1 { font-size: 24px; font-weight: bold; margin: 0 0 8px; } -.subtitle { font-size: 12px; color: var(--muted); margin: 0; } - -.status-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} -.status-row .label { flex: 1; } -.status-value { flex: 2; color: var(--ok); } - -.btn-small { font-size: 12px; padding: 4px 12px; } - -.section-title { font-size: 14px; font-weight: bold; margin: 8px 0 4px; } - -.neural-context { - height: 150px; - overflow-y: auto; - background: var(--panel); - padding: 8px; - margin-bottom: 16px; - font-family: monospace; - font-size: 11px; - white-space: pre-wrap; -} - -textarea#inputField { - width: 100%; - min-height: 3em; - margin-bottom: 8px; - font: inherit; - padding: 8px; - resize: vertical; -} -textarea#inputField:disabled { background: #eee; } - -.query-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} -.switch-label { display: inline-flex; align-items: center; gap: 6px; } -.spacer { flex: 1; } - -.spinner { - width: 20px; - height: 20px; - border: 3px solid #ccc; - border-top-color: var(--ok); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} -.spinner[hidden] { display: none; } -@keyframes spin { to { transform: rotate(360deg); } } - -.response { - flex: 1; - overflow-y: auto; - background: var(--panel-2); - padding: 8px; - font-size: 14px; - white-space: pre-wrap; -} - -footer { - text-align: center; - font-size: 10px; - color: var(--faint); - margin-top: 8px; -} diff --git a/android/app/src/main/java/ai/neurophone/BootReceiver.java b/android/app/src/main/java/ai/neurophone/BootReceiver.java deleted file mode 100644 index 5d83702..0000000 --- a/android/app/src/main/java/ai/neurophone/BootReceiver.java +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -package ai.neurophone; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -/** - * Thin {@link BroadcastReceiver} shim that restarts the neurophone foreground - * {@link NeurophoneService} after the device finishes booting. - * - *

This is a deliberately minimal, hand-written Java shim that replaces the - * previous Kotlin {@code BootReceiver}. It is part of the Android Kotlin to - * Rust/Gossamer migration (epic #83). The receiver contains no - * business logic: any decision about whether the service should actually run - * (e.g. honouring persisted "was running" state) belongs in the Rust JNI layer - * ({@code crates/neurophone-android}) and is reached through the service start - * path, not here. - * - *

Hand-written Java is permitted only under {@code android/} via the - * {@code .hypatia-baseline.json} exemption for the in-flight Gossamer - * migration. - * - *

TODO(#83): once the Rust JNI boot-policy entrypoint lands, delegate the - * "should we restart?" decision to {@code crates/neurophone-android} rather - * than unconditionally starting the service. - *

TODO(#83 rebase): depends on sub-PRs #4 (NativeLib to Rust) and #5 - * (Service shim); re-point the {@code NeurophoneService} reference if those - * sub-PRs rename or relocate the service entrypoint. - */ -public final class BootReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - if (context == null || intent == null) { - return; - } - final String action = intent.getAction(); - if (!Intent.ACTION_BOOT_COMPLETED.equals(action) - && !Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) { - return; - } - // Thin shim: start the foreground service from sub-PR #5. All runtime - // policy and inference lives behind the Rust JNI in NeurophoneService. - final Intent serviceIntent = new Intent(context, NeurophoneService.class); - context.startForegroundService(serviceIntent); - } -} diff --git a/android/app/src/main/java/ai/neurophone/BootReceiver.kt b/android/app/src/main/java/ai/neurophone/BootReceiver.kt deleted file mode 100644 index 4fc6a01..0000000 --- a/android/app/src/main/java/ai/neurophone/BootReceiver.kt +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -package ai.neurophone - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -/** - * Restart the foreground service after device reboot, but only if the user - * had it running before. - * - * TODO(#83 rebase): the "was running" flag used to live in the widget's - * SharedPreferences (NeurophoneAppWidget.prefs / KEY_RUNNING), which was - * removed when the widget was ported to a stateless Java shim that reads the - * Rust core directly. Persisting the desired-run flag is now a core concern; - * sub-PR #3/#4/#5 is expected to expose it (e.g. NativeLib.shouldAutostart()). - * Until then, conservatively do nothing on boot rather than force-start. - */ -class BootReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action ?: return - if (action != Intent.ACTION_BOOT_COMPLETED && action != Intent.ACTION_LOCKED_BOOT_COMPLETED) { - return - } - // TODO(#83 rebase): wire to core-persisted autostart flag once available. - } -} diff --git a/android/app/src/main/java/ai/neurophone/MainActivity.kt b/android/app/src/main/java/ai/neurophone/MainActivity.kt deleted file mode 100644 index 0c07302..0000000 --- a/android/app/src/main/java/ai/neurophone/MainActivity.kt +++ /dev/null @@ -1,377 +0,0 @@ -package ai.neurophone - -import android.Manifest -import android.content.pm.PackageManager -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.* -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import kotlinx.coroutines.* -import org.json.JSONObject - -/** - * Main activity for the NeuroSymbolic Phone application. - * Manages sensor input, neural processing, and LLM interaction. - */ -class MainActivity : AppCompatActivity(), SensorEventListener { - - companion object { - private const val TAG = "NeuroPhone" - private const val PERMISSION_REQUEST_CODE = 100 - } - - // UI Components - private lateinit var statusText: TextView - private lateinit var neuralContextText: TextView - private lateinit var inputField: EditText - private lateinit var responseText: TextView - private lateinit var startButton: Button - private lateinit var sendButton: Button - private lateinit var localSwitch: Switch - private lateinit var activityIndicator: ProgressBar - - // Sensor management - private lateinit var sensorManager: SensorManager - private var accelerometer: Sensor? = null - private var gyroscope: Sensor? = null - private var magnetometer: Sensor? = null - private var lightSensor: Sensor? = null - private var proximitySensor: Sensor? = null - - // Coroutine scope for background tasks - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - // State - private var isSystemRunning = false - private var contextUpdateJob: Job? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - initializeViews() - initializeSensors() - initializeSystem() - - checkPermissions() - handleEntryIntent(intent) - } - - override fun onNewIntent(intent: android.content.Intent) { - super.onNewIntent(intent) - handleEntryIntent(intent) - } - - /** - * Handle entry from the home-screen widget, share sheet, ASSIST gesture, - * or `neurophone://` deep link. - */ - private fun handleEntryIntent(intent: android.content.Intent?) { - intent ?: return - when (intent.action) { - ai.neurophone.widget.NeurophoneAppWidget.ACTION_QUERY -> { - inputField.requestFocus() - } - android.content.Intent.ACTION_SEND -> { - val text = intent.getStringExtra(android.content.Intent.EXTRA_TEXT) - if (!text.isNullOrBlank()) { - inputField.setText(text) - if (isSystemRunning) sendQuery() - } - } - android.content.Intent.ACTION_VIEW -> { - intent.data?.let { uri -> - if (uri.scheme == "neurophone") { - uri.getQueryParameter("q")?.let { - inputField.setText(it) - if (isSystemRunning) sendQuery() - } - } - } - } - android.content.Intent.ACTION_ASSIST -> { - inputField.requestFocus() - } - } - } - - private fun initializeViews() { - statusText = findViewById(R.id.statusText) - neuralContextText = findViewById(R.id.neuralContextText) - inputField = findViewById(R.id.inputField) - responseText = findViewById(R.id.responseText) - startButton = findViewById(R.id.startButton) - sendButton = findViewById(R.id.sendButton) - localSwitch = findViewById(R.id.localSwitch) - activityIndicator = findViewById(R.id.activityIndicator) - - startButton.setOnClickListener { toggleSystem() } - sendButton.setOnClickListener { sendQuery() } - - updateUI() - } - - private fun initializeSensors() { - sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager - - accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) - magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) - lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) - proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) - - Log.d(TAG, "Sensors initialized: " + - "accel=${accelerometer != null}, " + - "gyro=${gyroscope != null}, " + - "mag=${magnetometer != null}, " + - "light=${lightSensor != null}, " + - "prox=${proximitySensor != null}") - } - - private fun initializeSystem() { - scope.launch(Dispatchers.IO) { - try { - val config = createConfig() - val success = NativeLib.init(config) - - withContext(Dispatchers.Main) { - if (success) { - statusText.text = "System initialized" - Log.i(TAG, "Native system initialized") - } else { - statusText.text = "Initialization failed" - Log.e(TAG, "Failed to initialize native system") - } - } - } catch (e: Exception) { - Log.e(TAG, "Error initializing system", e) - withContext(Dispatchers.Main) { - statusText.text = "Error: ${e.message}" - } - } - } - } - - private fun createConfig(): String { - return JSONObject().apply { - put("loop_interval_ms", 20) - put("debug", false) - // Sensor config - put("sensor", JSONObject().apply { - put("sample_rate_hz", 50.0) - put("buffer_size", 100) - put("output_dim", 32) - }) - // LSM config (optimized for Oppo Reno 13) - put("lsm", JSONObject().apply { - put("dimensions", listOf(8, 8, 8)) - put("spectral_radius", 0.9) - }) - // ESN config - put("esn", JSONObject().apply { - put("reservoir_size", 300) - put("spectral_radius", 0.95) - }) - }.toString() - } - - private fun toggleSystem() { - if (isSystemRunning) { - stopSystem() - } else { - startSystem() - } - } - - private fun startSystem() { - scope.launch(Dispatchers.IO) { - val success = NativeLib.start() - - withContext(Dispatchers.Main) { - if (success) { - isSystemRunning = true - registerSensors() - startContextUpdates() - updateUI() - statusText.text = "System running" - } else { - statusText.text = "Failed to start" - } - } - } - } - - private fun stopSystem() { - scope.launch(Dispatchers.IO) { - NativeLib.stop() - - withContext(Dispatchers.Main) { - isSystemRunning = false - unregisterSensors() - stopContextUpdates() - updateUI() - statusText.text = "System stopped" - } - } - } - - private fun registerSensors() { - val samplingPeriod = SensorManager.SENSOR_DELAY_GAME // ~20ms - - accelerometer?.let { sensorManager.registerListener(this, it, samplingPeriod) } - gyroscope?.let { sensorManager.registerListener(this, it, samplingPeriod) } - magnetometer?.let { sensorManager.registerListener(this, it, samplingPeriod) } - lightSensor?.let { sensorManager.registerListener(this, it, samplingPeriod) } - proximitySensor?.let { sensorManager.registerListener(this, it, samplingPeriod) } - - Log.d(TAG, "Sensors registered") - } - - private fun unregisterSensors() { - sensorManager.unregisterListener(this) - Log.d(TAG, "Sensors unregistered") - } - - private fun startContextUpdates() { - contextUpdateJob = scope.launch { - while (isActive && isSystemRunning) { - try { - val context = withContext(Dispatchers.IO) { - NativeLib.getNeuralContext() - } - neuralContextText.text = context - } catch (e: Exception) { - Log.w(TAG, "Error updating context", e) - } - delay(500) // Update every 500ms - } - } - } - - private fun stopContextUpdates() { - contextUpdateJob?.cancel() - contextUpdateJob = null - } - - private fun sendQuery() { - val message = inputField.text.toString().trim() - if (message.isEmpty()) { - Toast.makeText(this, "Please enter a message", Toast.LENGTH_SHORT).show() - return - } - - activityIndicator.visibility = View.VISIBLE - sendButton.isEnabled = false - - scope.launch { - try { - val preferLocal = localSwitch.isChecked - val response = withContext(Dispatchers.IO) { - NativeLib.query(message, preferLocal) - } - - responseText.text = response.ifEmpty { "No response received" } - } catch (e: Exception) { - Log.e(TAG, "Query error", e) - responseText.text = "Error: ${e.message}" - } finally { - activityIndicator.visibility = View.GONE - sendButton.isEnabled = true - } - } - } - - private fun updateUI() { - startButton.text = if (isSystemRunning) "Stop" else "Start" - sendButton.isEnabled = isSystemRunning - inputField.isEnabled = isSystemRunning - } - - // SensorEventListener implementation - override fun onSensorChanged(event: SensorEvent) { - if (!isSystemRunning) return - - // Send sensor data to native system - scope.launch(Dispatchers.IO) { - NativeLib.processSensor( - event.sensor.type, - event.values.clone(), - event.timestamp, - event.accuracy - ) - } - } - - override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { - Log.d(TAG, "Sensor ${sensor.name} accuracy changed to $accuracy") - } - - // Permissions - private fun checkPermissions() { - val permissions = mutableListOf() - - // Body sensors permission (for heart rate, etc. if available) - if (ContextCompat.checkSelfPermission(this, Manifest.permission.BODY_SENSORS) - != PackageManager.PERMISSION_GRANTED) { - permissions.add(Manifest.permission.BODY_SENSORS) - } - - // Internet permission for Claude API - if (ContextCompat.checkSelfPermission(this, Manifest.permission.INTERNET) - != PackageManager.PERMISSION_GRANTED) { - permissions.add(Manifest.permission.INTERNET) - } - - if (permissions.isNotEmpty()) { - ActivityCompat.requestPermissions( - this, - permissions.toTypedArray(), - PERMISSION_REQUEST_CODE - ) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - if (requestCode == PERMISSION_REQUEST_CODE) { - val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } - if (!allGranted) { - Toast.makeText(this, "Some permissions denied", Toast.LENGTH_LONG).show() - } - } - } - - override fun onResume() { - super.onResume() - if (isSystemRunning) { - registerSensors() - startContextUpdates() - } - } - - override fun onPause() { - super.onPause() - unregisterSensors() - stopContextUpdates() - } - - override fun onDestroy() { - super.onDestroy() - scope.cancel() - if (isSystemRunning) { - NativeLib.stop() - } - } -} diff --git a/android/app/src/main/java/ai/neurophone/NativeLib.kt b/android/app/src/main/java/ai/neurophone/NativeLib.kt deleted file mode 100644 index 454b605..0000000 --- a/android/app/src/main/java/ai/neurophone/NativeLib.kt +++ /dev/null @@ -1,93 +0,0 @@ -package ai.neurophone - -/** - * Native library interface for the NeuroSymbolic system. - * Connects Kotlin/Android to the Rust core via JNI. - */ -object NativeLib { - - init { - System.loadLibrary("neurophone_android") - } - - /** - * Initialize the native system with optional JSON config - */ - external fun init(configJson: String? = null): Boolean - - /** - * Start the neural processing loop - */ - external fun start(): Boolean - - /** - * Stop the neural processing loop - */ - external fun stop() - - /** - * Process sensor data - * @param sensorType Android sensor type constant - * @param values Sensor values array - * @param timestamp Event timestamp in nanoseconds - * @param accuracy Sensor accuracy level - */ - external fun processSensor( - sensorType: Int, - values: FloatArray, - timestamp: Long, - accuracy: Int - ): Boolean - - /** - * Query local LLM (Llama 3.2) - */ - external fun queryLocal(message: String): String - - /** - * Query Claude (cloud) - */ - external fun queryClaude(message: String): String - - /** - * Smart query - auto-selects local or cloud - */ - external fun query(message: String, preferLocal: Boolean = true): String - - /** - * Get current neural context as formatted string - */ - external fun getNeuralContext(): String - - /** - * Get system state as JSON - */ - external fun getState(): String - - /** - * Reset all neural components - */ - external fun reset() - - /** - * Check if system is running - */ - external fun isRunning(): Boolean - - /** - * Convenience wrapper used by the foreground service so the call site - * is symmetric with the rest of the API even though the JNI signature - * matches `processSensor` underneath. - */ - fun pushSensorEvent(sensorType: String, timestampMs: Long, values: FloatArray): Boolean { - val typeId = when (sensorType) { - "accelerometer" -> 1 - "gyroscope" -> 4 - "magnetometer" -> 2 - "light" -> 5 - "proximity" -> 8 - else -> 0 - } - return processSensor(typeId, values, timestampMs * 1_000_000L, 3) - } -} diff --git a/android/app/src/main/java/ai/neurophone/NeurophoneService.java b/android/app/src/main/java/ai/neurophone/NeurophoneService.java deleted file mode 100644 index 9d6e001..0000000 --- a/android/app/src/main/java/ai/neurophone/NeurophoneService.java +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -package ai.neurophone; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.content.pm.ServiceInfo; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.os.Build; -import android.os.IBinder; -import android.os.PowerManager; - -/** - * Thin foreground {@link Service} shim for the Gossamer migration. - * - *

All business logic (sensor → LSM → ESN → bridge loop, - * salience computation, widget publishing) now lives in Rust behind the - * {@code crates/neurophone-android} JNI boundary. This Java shim only: - *

    - *
  • holds the foreground notification + wake lock so Android keeps us - * alive,
  • - *
  • calls {@link NativeLib#start()} on create and {@link NativeLib#stop()} - * on destroy,
  • - *
  • forwards raw sensor events into Rust via - * {@link NativeLib#processSensor(int, float[], long, int)}.
  • - *
- * - *

Hand-written Java is permitted only under {@code android/} (see - * {@code .hypatia-baseline.json}); every method below is deliberately minimal. - * - *

TODO(#83): this shim is the migration seam. Once sub-PR #4 - * (NativeLib→Rust JNI) and sub-PR #3 (Gossamer scaffolding) land, the - * remaining Android-owned concerns (foreground notification, wake lock, - * sensor registration) should move behind Gossamer/Rust and this file should - * shrink further or be removed entirely with the rest of {@code android/}. - */ -public final class NeurophoneService extends Service implements SensorEventListener { - - private static final String CHANNEL_ID = "neurophone_runtime"; - private static final int NOTIF_ID = 0x4E50; // 'NP' - - /** Sensor sampling cadence; ~50 Hz, matching the prior Kotlin service. */ - private static final int SENSOR_DELAY = SensorManager.SENSOR_DELAY_GAME; - - /** Default reported accuracy when a real value is unavailable. */ - private static final int DEFAULT_ACCURACY = 3; // SENSOR_STATUS_ACCURACY_HIGH - - private SensorManager sensorManager; - private PowerManager.WakeLock wakeLock; - - @Override - public void onCreate() { - super.onCreate(); - ensureChannel(); - - sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); - - PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "neurophone:service"); - wakeLock.setReferenceCounted(false); - wakeLock.acquire(10 * 60 * 1000L); - - // TODO(#83 rebase): NativeLib is a Kotlin `object`; once sub-PR #4 lands - // the Rust JNI library `neurophone_android` must export init/start/stop/ - // processSensor. Guard so dev hardware without the native lib still runs. - try { - NativeLib.INSTANCE.init(null); - NativeLib.INSTANCE.start(); - } catch (Throwable t) { - // dev mode: no native library present yet - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - startForegroundCompat(); - registerSensors(); - return START_STICKY; - } - - @Override - public void onDestroy() { - if (sensorManager != null) { - sensorManager.unregisterListener(this); - } - try { - NativeLib.INSTANCE.stop(); - } catch (Throwable t) { - // dev mode: no native library present yet - } - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - } - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onSensorChanged(SensorEvent event) { - int typeId = typeIdFor(event.sensor.getType()); - // Replicate the prior Kotlin convenience mapping: callers identified - // sensors by name, here we collapse to the same numeric id space and - // forward straight to Rust. timestamp ms -> ns. - long timestampNs = System.currentTimeMillis() * 1_000_000L; - try { - NativeLib.INSTANCE.processSensor(typeId, event.values, timestampNs, DEFAULT_ACCURACY); - } catch (Throwable t) { - // dev mode without JNI - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - // no-op - } - - /** - * Maps an Android {@link Sensor} type constant to the compact id space the - * Rust core expects. Mirrors the sensor-name → id table that lived in - * {@code NativeLib.pushSensorEvent} on the Kotlin side: - * accelerometer=1, magnetometer=2, gyroscope=4, light=5, proximity=8, - * everything else=0. - */ - private static int typeIdFor(int sensorType) { - switch (sensorType) { - case Sensor.TYPE_ACCELEROMETER: - return 1; - case Sensor.TYPE_MAGNETIC_FIELD: - return 2; - case Sensor.TYPE_GYROSCOPE: - return 4; - case Sensor.TYPE_LIGHT: - return 5; - case Sensor.TYPE_PROXIMITY: - return 8; - default: - return 0; - } - } - - private void registerSensors() { - if (sensorManager == null) { - return; - } - // TODO(#83): widen beyond accelerometer once the Rust core consumes the - // full sensor set; the type-id mapping above already covers them. - registerIfPresent(Sensor.TYPE_ACCELEROMETER); - } - - private void registerIfPresent(int sensorType) { - Sensor sensor = sensorManager.getDefaultSensor(sensorType); - if (sensor != null) { - sensorManager.registerListener(this, sensor, SENSOR_DELAY); - } - } - - private void startForegroundCompat() { - PendingIntent open = PendingIntent.getActivity( - this, 0, - new Intent(this, MainActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - - Notification notif = new Notification.Builder(this, CHANNEL_ID) - .setContentTitle(getString(R.string.service_notification_title)) - .setContentText(getString(R.string.service_notification_text)) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setContentIntent(open) - .setOngoing(true) - .build(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); - } else { - startForeground(NOTIF_ID, notif); - } - } - - private void ensureChannel() { - NotificationManager nm = getSystemService(NotificationManager.class); - if (nm.getNotificationChannel(CHANNEL_ID) == null) { - NotificationChannel ch = new NotificationChannel( - CHANNEL_ID, - getString(R.string.service_channel_name), - NotificationManager.IMPORTANCE_LOW); - ch.setDescription(getString(R.string.service_channel_desc)); - nm.createNotificationChannel(ch); - } - } -} diff --git a/android/app/src/main/java/ai/neurophone/NeurophoneService.kt b/android/app/src/main/java/ai/neurophone/NeurophoneService.kt deleted file mode 100644 index 8994731..0000000 --- a/android/app/src/main/java/ai/neurophone/NeurophoneService.kt +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -package ai.neurophone - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Intent -import android.content.pm.ServiceInfo -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.os.Build -import android.os.Handler -import android.os.IBinder -import android.os.Looper -import android.os.PowerManager -import ai.neurophone.widget.NeurophoneAppWidget - -/** - * Long-running foreground service that owns the sensor → LSM → ESN → bridge - * loop. Pushes neural state to the home-screen widget every 1 s. - * - * Native (Rust) inference goes through `NativeLib`. While that JNI stack is - * being completed this service emits a synthetic but smooth salience signal - * derived from accelerometer variance, so the widget visibly responds even - * when running on dev hardware without the full LLM. - */ -class NeurophoneService : Service(), SensorEventListener { - - private lateinit var sensorManager: SensorManager - private var accel: Sensor? = null - private val handler = Handler(Looper.getMainLooper()) - private var wakeLock: PowerManager.WakeLock? = null - - // Rolling stats for synthetic salience (variance of accel magnitude). - private val window = ArrayDeque() - private val windowCap = 50 - - private val tickRunnable = object : Runnable { - override fun run() { - publishWidgetState() - handler.postDelayed(this, 1_000) - } - } - - override fun onCreate() { - super.onCreate() - ensureChannel() - sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager - accel = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - - val pm = getSystemService(POWER_SERVICE) as PowerManager - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "neurophone:service").apply { - setReferenceCounted(false) - acquire(10 * 60 * 1000L) - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - startForegroundCompat() - accel?.let { - sensorManager.registerListener( - this, it, - SensorManager.SENSOR_DELAY_GAME // ~50 Hz - ) - } - try { NativeLib.init(); NativeLib.start() } catch (_: Throwable) { /* dev mode */ } - handler.post(tickRunnable) - return START_STICKY - } - - override fun onDestroy() { - handler.removeCallbacks(tickRunnable) - sensorManager.unregisterListener(this) - try { NativeLib.stop() } catch (_: Throwable) {} - wakeLock?.let { if (it.isHeld) it.release() } - // TODO(#83 rebase): widget now reads live state from the Rust core - // (NativeLib.getState()); we only nudge it to re-render. The old - // publishState(...) SharedPreferences push was dropped with the - // Java widget shim port. - NeurophoneAppWidget.requestRefresh(this) - super.onDestroy() - } - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onSensorChanged(event: SensorEvent) { - if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) return - val mag = kotlin.math.sqrt( - (event.values[0] * event.values[0] + - event.values[1] * event.values[1] + - event.values[2] * event.values[2]).toDouble() - ).toFloat() - if (window.size == windowCap) window.removeFirst() - window.addLast(mag) - - try { - NativeLib.pushSensorEvent( - "accelerometer", - System.currentTimeMillis(), - event.values - ) - } catch (_: Throwable) { /* dev mode without JNI */ } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { /* no-op */ } - - private fun publishWidgetState() { - // TODO(#83 rebase): the widget reads salience/description straight from - // the Rust core via NativeLib.getState() now, so the service no longer - // pushes a snapshot — it just asks the widget to re-render. computeSalience() - // is retained for the in-service tick/notification path. - NeurophoneAppWidget.requestRefresh(this) - } - - private fun computeSalience(): Float { - if (window.size < 5) return 0f - val mean = window.average().toFloat() - val variance = window.sumOf { ((it - mean) * (it - mean)).toDouble() }.toFloat() / window.size - // Normalise: 0 at rest (~9.81 m/s² constant), 1 at vigorous shake (variance ≥ 25). - return (variance / 25f).coerceIn(0f, 1f) - } - - private fun startForegroundCompat() { - val open = PendingIntent.getActivity( - this, 0, - Intent(this, MainActivity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val notif: Notification = Notification.Builder(this, CHANNEL_ID) - .setContentTitle(getString(R.string.service_notification_title)) - .setContentText(getString(R.string.service_notification_text)) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setContentIntent(open) - .setOngoing(true) - .build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) - } else { - startForeground(NOTIF_ID, notif) - } - } - - private fun ensureChannel() { - val nm = getSystemService(NotificationManager::class.java) - if (nm.getNotificationChannel(CHANNEL_ID) == null) { - val ch = NotificationChannel( - CHANNEL_ID, - getString(R.string.service_channel_name), - NotificationManager.IMPORTANCE_LOW - ).apply { description = getString(R.string.service_channel_desc) } - nm.createNotificationChannel(ch) - } - } - - companion object { - private const val CHANNEL_ID = "neurophone_runtime" - private const val NOTIF_ID = 0x4E50 // 'NP' - } -} diff --git a/android/app/src/main/java/ai/neurophone/gossamer/NeurophoneMainActivity.java b/android/app/src/main/java/ai/neurophone/gossamer/NeurophoneMainActivity.java deleted file mode 100644 index cddd7e6..0000000 --- a/android/app/src/main/java/ai/neurophone/gossamer/NeurophoneMainActivity.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2025 Jonathan D.A. Jewell (hyperpolymath) - -package ai.neurophone.gossamer; - -import io.gossamer.GossamerActivity; - -/** - * NeurophoneMainActivity — Gossamer entry-point Activity for the NeuroPhone - * Android app. - * - *

This is the scaffold introduced by sub-PR #3 of the Kotlin->Rust/Gossamer - * migration epic (#83, RFC #97, tracking sub-issue #109). It extends - * {@link io.gossamer.GossamerActivity} (from the {@code gossamer} Android - * library, package {@code io.gossamer}), which hosts a full-screen WebView, - * loads {@code libgossamer.so} via {@code System.loadLibrary("gossamer")} in a - * static initialiser, and registers the {@code GossamerBridge} JavaScript - * interface for native IPC. - * - *

Gossamer on Android is webview-only today, so this scaffold only overrides - * {@link #getInitialHtml()} to render placeholder content. No native NeuroPhone - * code is wired in yet. - * - *

This shim deliberately does NOT replace the legacy Kotlin - * {@code ai.neurophone.MainActivity}, {@code NeurophoneService}, - * {@code BootReceiver}, {@code NativeLib}, or the widgets — those are ported in - * later sub-PRs. The {@code android/} subtree is exempt from the banned-language - * CI gate (see {@code .hypatia-baseline.json}, tracking #97), so this - * hand-written Java shim is permitted. - * - *

TODO(#83 sub-PR #4): replace the placeholder HTML with the real bundled web - * UI by overriding {@link #getInitialUrl()} (or pointing - * {@code gossamer.conf.json}'s {@code build.frontendDist} at it). - *

TODO(#83 sub-PR #5): port {@code ai.neurophone.NativeLib}'s 11 JNI methods - * (init/start/stop/processSensor/queryLocal/queryClaude/query/ - * getNeuralContext/getState/reset/isRunning) onto the Gossamer IPC bridge, - * backed by the Rust core in {@code crates/neurophone-android} / - * {@code crates/neurophone-core}. - *

TODO(#83 sub-PR #6): port the sensor pipeline (accelerometer/gyroscope/ - * magnetometer/light/proximity) feeding the LSM->ESN loop. - *

TODO(#83 sub-PR #7): port {@code NeurophoneService} (foreground service), - * {@code BootReceiver}, and the home-screen widgets. - */ -public class NeurophoneMainActivity extends GossamerActivity { - - /** - * Placeholder content for the Gossamer WebView. - * - *

Returning non-null HTML makes {@link GossamerActivity} call - * {@code webView.loadData(...)} instead of loading a URL. Replaced by the - * real UI in sub-PR #4. - */ - @Override - protected String getInitialHtml() { - // TODO(#83 sub-PR #4): remove this placeholder once the real frontend is bundled. - return "" - + "" - + "" - + "NeuroPhone" - + "" - + "

NeuroPhone

" - + "

Gossamer scaffold (sub-PR #3). UI and native bridge land in sub-PRs #4-#7.

" - + ""; - } -} diff --git a/android/app/src/main/java/ai/neurophone/widget/NeurophoneAppWidget.java b/android/app/src/main/java/ai/neurophone/widget/NeurophoneAppWidget.java deleted file mode 100644 index 089d9eb..0000000 --- a/android/app/src/main/java/ai/neurophone/widget/NeurophoneAppWidget.java +++ /dev/null @@ -1,198 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -package ai.neurophone.widget; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.widget.RemoteViews; - -import org.json.JSONObject; - -import ai.neurophone.MainActivity; -import ai.neurophone.NativeLib; -import ai.neurophone.NeurophoneService; -import ai.neurophone.R; - -/** - * Home-screen widget for NeuroPhone. - * - *

Thin hand-written Java {@link AppWidgetProvider} shim. It owns no neural - * state of its own: every render reads the live system state straight out of - * the Rust core via {@link NativeLib#getState()} (a JSON string) and the "Ask" - * action funnels a query through {@link NativeLib#query(String, boolean)}. - * - *

Layout: title bar + state line + salience meter + power/refresh/ask - * buttons. Three intent actions: - *

    - *
  • {@code ACTION_TOGGLE} -> start/stop the foreground service
  • - *
  • {@code ACTION_REFRESH} -> re-read state from the core and redraw
  • - *
  • {@code ACTION_QUERY} -> open MainActivity in query mode
  • - *
- * - *

This is part of the Android Kotlin->Rust/Gossamer migration (epic #83). - * It replaces the former Kotlin {@code NeurophoneAppWidget.kt}; the prior - * SharedPreferences-backed {@code publishState(...)} path is gone because the - * Rust core is now the single source of truth. The configure activity was - * dropped by owner decision, so the widget is fully usable with no setup step. - */ -public final class NeurophoneAppWidget extends AppWidgetProvider { - - public static final String ACTION_REFRESH = "ai.neurophone.widget.ACTION_REFRESH"; - public static final String ACTION_TOGGLE = "ai.neurophone.widget.ACTION_TOGGLE"; - public static final String ACTION_QUERY = "ai.neurophone.widget.ACTION_QUERY"; - - @Override - public void onUpdate(Context context, AppWidgetManager manager, int[] ids) { - for (int id : ids) { - render(context, manager, id); - } - } - - @Override - public void onReceive(Context context, Intent intent) { - super.onReceive(context, intent); - final String action = intent.getAction(); - if (ACTION_REFRESH.equals(action) - || ACTION_TOGGLE.equals(action) - || ACTION_QUERY.equals(action)) { - if (ACTION_TOGGLE.equals(action)) { - toggleService(context); - } - final AppWidgetManager mgr = AppWidgetManager.getInstance(context); - final int[] ids = mgr.getAppWidgetIds( - new ComponentName(context, NeurophoneAppWidget.class)); - for (int id : ids) { - render(context, mgr, id); - } - } - } - - /** - * Start/stop the foreground service. The running/stopped truth is read back - * from the Rust core on the next render via {@link NativeLib#isRunning()}, - * so we do not cache it locally. - */ - private void toggleService(Context context) { - final Intent svc = new Intent(context, NeurophoneService.class); - // TODO(#83 rebase): isRunning() ships with the JNI bridge in sub-PR - // #3/#4/#5; until merged the call resolves against the stub NativeLib. - if (nativeIsRunning()) { - context.stopService(svc); - } else { - context.startForegroundService(svc); - } - } - - /** - * Read fresh neural state from the Rust core and push it into the - * RemoteViews. No SharedPreferences, no app-side state. - */ - private void render(Context context, AppWidgetManager mgr, int id) { - final RemoteViews views = - new RemoteViews(context.getPackageName(), R.layout.widget_neurophone); - - final boolean running = nativeIsRunning(); - float salience = 0f; - String description = null; - - // NativeLib.getState() returns a JSON snapshot of the core. Parse - // defensively: the widget must never crash the launcher on a malformed - // or empty payload (e.g. before the service has started). - // TODO(#83 rebase): the concrete JSON schema is finalised alongside the - // JNI bridge in sub-PR #3/#4/#5. Keys below are the agreed contract; - // confirm on rebase and tighten if the schema changes. - try { - final String stateJson = NativeLib.INSTANCE.getState(); - if (stateJson != null && !stateJson.isEmpty()) { - final JSONObject state = new JSONObject(stateJson); - salience = (float) state.optDouble("salience", 0d); - description = state.optString("description", null); - } - } catch (Throwable t) { - // Swallow: fall back to running/stopped string below. - description = null; - } - - if (salience < 0f) { - salience = 0f; - } else if (salience > 1f) { - salience = 1f; - } - - views.setTextViewText( - R.id.widget_state, - description != null && !description.isEmpty() - ? description - : context.getString(running - ? R.string.widget_state_running - : R.string.widget_state_stopped)); - - final int saliencePct = (int) (salience * 100f); - views.setProgressBar(R.id.widget_salience, 100, saliencePct, false); - views.setTextViewText(R.id.widget_salience_value, saliencePct + "%"); - - views.setOnClickPendingIntent( - R.id.widget_toggle, actionPI(context, ACTION_TOGGLE, id, 1)); - views.setOnClickPendingIntent( - R.id.widget_refresh, actionPI(context, ACTION_REFRESH, id, 2)); - views.setOnClickPendingIntent( - R.id.widget_query, queryPI(context, id)); - - mgr.updateAppWidget(id, views); - } - - private PendingIntent actionPI(Context context, String action, int widgetId, int requestCode) { - final Intent intent = new Intent(context, NeurophoneAppWidget.class); - intent.setAction(action); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); - final int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast( - context, requestCode * 100 + widgetId, intent, flags); - } - - private PendingIntent queryPI(Context context, int widgetId) { - final Intent intent = new Intent(context, MainActivity.class); - intent.setAction(ACTION_QUERY); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - final int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getActivity(context, 1000 + widgetId, intent, flags); - } - - /** - * Ask the Rust core whether the loop is running. Isolated so the one - * cross-language interop point is easy to retarget on the #83 rebase. - */ - private static boolean nativeIsRunning() { - // TODO(#83 rebase): NativeLib is currently the Kotlin `object` from the - // pre-migration tree, hence the `.INSTANCE` interop. Sub-PR #3/#4/#5 - // may republish it as a Java class or static facade; drop `.INSTANCE` - // then. Guard against the stub throwing UnsatisfiedLinkError. - try { - return NativeLib.INSTANCE.isRunning(); - } catch (Throwable t) { - return false; - } - } - - /** - * Broadcast a refresh to every mounted instance of this widget. Used by - * {@link NeurophoneWidgetActions} and other non-widget callers (service, - * boot receiver) to nudge the widget into re-reading core state. - */ - public static void requestRefresh(Context context) { - final AppWidgetManager mgr = AppWidgetManager.getInstance(context); - final int[] ids = mgr.getAppWidgetIds( - new ComponentName(context, NeurophoneAppWidget.class)); - if (ids.length == 0) { - return; - } - final Intent refresh = new Intent(context, NeurophoneAppWidget.class); - refresh.setAction(ACTION_REFRESH); - refresh.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - context.sendBroadcast(refresh); - } -} diff --git a/android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetActions.java b/android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetActions.java deleted file mode 100644 index 3f306b3..0000000 --- a/android/app/src/main/java/ai/neurophone/widget/NeurophoneWidgetActions.java +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell -package ai.neurophone.widget; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import ai.neurophone.NativeLib; - -/** - * Lightweight broadcast receiver dispatched by non-widget callers (the - * foreground service, the boot receiver, the share-intent handler) to drive - * the widget without holding a reference to it. - * - *

Thin hand-written Java {@link BroadcastReceiver} shim, part of the Android - * Kotlin->Rust/Gossamer migration (epic #83). It replaces the former Kotlin - * {@code NeurophoneWidgetActions.kt}. - * - *

The pre-migration {@code PUBLISH_STATE} path carried neural state in the - * intent extras and stashed it in SharedPreferences. That is gone: the Rust - * core is now the single source of truth, so callers only need to nudge the - * widget into re-reading it via {@link NativeLib#getState()}. - * - *

Two actions: - *

    - *
  • {@code ACTION_FORCE_REFRESH} -> re-render every mounted widget
  • - *
  • {@code ACTION_QUERY} -> run a one-shot query against the core via - * {@link NativeLib#query(String, boolean)}, then refresh
  • - *
- */ -public final class NeurophoneWidgetActions extends BroadcastReceiver { - - public static final String ACTION_FORCE_REFRESH = "ai.neurophone.widget.FORCE_REFRESH"; - public static final String ACTION_QUERY = "ai.neurophone.widget.PUBLISH_QUERY"; - - /** Query text carried by {@link #ACTION_QUERY}. */ - public static final String EXTRA_QUERY = "query"; - /** Prefer the on-device model over the cloud fallback. Defaults to true. */ - public static final String EXTRA_PREFER_LOCAL = "prefer_local"; - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - if (ACTION_FORCE_REFRESH.equals(action)) { - NeurophoneAppWidget.requestRefresh(context); - } else if (ACTION_QUERY.equals(action)) { - final String query = intent.getStringExtra(EXTRA_QUERY); - final boolean preferLocal = intent.getBooleanExtra(EXTRA_PREFER_LOCAL, true); - if (query != null && !query.isEmpty()) { - runQuery(query, preferLocal); - } - // State may have moved as a result of the query; redraw from core. - NeurophoneAppWidget.requestRefresh(context); - } - } - - /** - * Fire a query into the Rust core. Result handling (surfacing the answer in - * a notification / activity) is owned elsewhere; here we only ensure the - * core advances so the next render reflects it. - */ - private static void runQuery(String query, boolean preferLocal) { - // TODO(#83 rebase): NativeLib is the Kotlin `object` from the - // pre-migration tree (hence `.INSTANCE`); sub-PR #3/#4/#5 may - // republish it as a Java facade. Until the JNI bridge lands the call - // resolves against the stub, so guard against UnsatisfiedLinkError. - try { - NativeLib.INSTANCE.query(query, preferLocal); - } catch (Throwable t) { - // No-op: a failed query must not crash the broadcasting caller. - } - } -} diff --git a/android/app/src/main/res/drawable/ic_widget_power.xml b/android/app/src/main/res/drawable/ic_widget_power.xml deleted file mode 100644 index e5974ba..0000000 --- a/android/app/src/main/res/drawable/ic_widget_power.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_widget_refresh.xml b/android/app/src/main/res/drawable/ic_widget_refresh.xml deleted file mode 100644 index 738d403..0000000 --- a/android/app/src/main/res/drawable/ic_widget_refresh.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/widget_background.xml b/android/app/src/main/res/drawable/widget_background.xml deleted file mode 100644 index d9b2cba..0000000 --- a/android/app/src/main/res/drawable/widget_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/drawable/widget_preview.xml b/android/app/src/main/res/drawable/widget_preview.xml deleted file mode 100644 index 13e7dd4..0000000 --- a/android/app/src/main/res/drawable/widget_preview.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 47924f3..0000000 --- a/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - - - - - - -