LSM + ESN + Llama 3.2 + Claude
-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 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:
- * 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 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 ""
- + " Gossamer scaffold (sub-PR #3). UI and native bridge land in sub-PRs #4-#7. 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:
- * 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:
- * Scaffolding only (epic #83, RFC PR #97, sub-issue #109, sub-PR #3). This
- * shim extends {@link io.gossamer.GossamerActivity} (from the gossamer Android
- * source tree, package {@code io.gossamer}) and overrides
- * {@link #getInitialHtml()} to display placeholder content in the Gossamer
- * WebView. It is the eventual replacement for the legacy Kotlin
- * {@code ai.neurophone.MainActivity} + its native-view UI.
- *
- * Gossamer on Android is webview-only today: {@code GossamerActivity}
- * creates a full-screen {@link android.webkit.WebView}, loads
- * {@code libgossamer.so} (Zig FFI) in a static block, registers the
- * {@code GossamerBridge} JavaScript interface, and calls {@code nativeInit()}.
- * Subclasses provide content by overriding {@code getInitialHtml()} or
- * {@code getInitialUrl()}.
- *
- * Out of scope for this sub-PR (deliberately NOT ported here):
- * TODO(#83 sub-PR #8): replace this inline placeholder with the real
- * NeuroPhone frontend served from {@code gossamer.conf.json}'s
- * {@code build.frontendDist} (or {@code getInitialUrl()} in dev), and wire
- * the UI to the Rust core via the Gossamer IPC bridge
- * ({@code window.GossamerBridge.postMessage}).
- */
- @Override
- protected String getInitialHtml() {
- return ""
- + " Webview scaffold is live. The real frontend and IPC wiring land in"
- + " later sub-PRs of the Android Kotlin→Rust/Gossamer migration"
- + " (
- *
- *
- * NeuroPhone
"
- + "
- *
- *
- *
- *
- */
-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 @@
-
-
-
- *
- */
-public class NeurophoneMainActivity extends GossamerActivity {
-
- /**
- * Placeholder HTML rendered in the Gossamer WebView.
- *
- * NeuroPhone · Gossamer
"
- + "#83).