diff --git a/.gitignore b/.gitignore index 9a41443..904178b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .theos/ packages/ obj/ +artifacts/ *.deb *.ipa *.dSYM/ diff --git a/README.md b/README.md index a80ae64..6a8d620 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Build one deployable Eve runtime family: - iOS: native full-screen renderer, multitouch controller, camera/mic/motion sensor publisher, and low-latency media display. - Android: native renderer and sensor publisher with the same CultNet contracts. +- Flutter: likely native client framework for Android/iOS parity once installed; + it should consume the browser-defined CultMesh surface model rather than + replacing the browser as ground truth. - Shared API: apps publish control surfaces and structured data; Eve publishes commands, pointer/touch input, and timestamped sensor packets. @@ -32,6 +35,11 @@ Build one deployable Eve runtime family: The current checked-in client is the iOS Theos app, still named `EveCanvas` at the bundle level until the wider Eve runtime split exists. +The Android proof under `android/` is a small native Java client that can be +built with the installed Android SDK without Gradle or Flutter. It exists to +put Eve on Periwinkle immediately: it shows the Eve/CultMesh role, polls the +Mimir dashboard broker, and surfaces timestamped touch/motion sensor data. + - `EVEAppDelegate` creates one fullscreen `UIWindow`. - `EVEViewController` installs: - `EVEGLView`, a `CAEAGLLayer` backed OpenGL ES render target. @@ -133,6 +141,26 @@ same VoidBot `swarm-state.json` projection used by the web dashboard. ## Build Shape +### Android / Periwinkle + +Build the Android proof APK from Starfire: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\build-android.ps1 +``` + +Install to Periwinkle: + +```powershell +& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" -s bad9dd01 install -r .\artifacts\android\eve-debug.apk +& "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe" -s bad9dd01 shell monkey -p org.gamecult.eve -c android.intent.category.LAUNCHER 1 +``` + +On Xiaomi/MIUI devices, ADB install may require enabling developer setting +`Install via USB` and approving the on-device prompt. + +### iOS / EVE + The project is a Theos-style iOS application: ```sh diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9826ec1 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/java/org/gamecult/eve/MainActivity.java b/android/app/src/main/java/org/gamecult/eve/MainActivity.java new file mode 100644 index 0000000..43ede9c --- /dev/null +++ b/android/app/src/main/java/org/gamecult/eve/MainActivity.java @@ -0,0 +1,196 @@ +package org.gamecult.eve; + +import android.app.Activity; +import android.graphics.Color; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public final class MainActivity extends Activity implements SensorEventListener { + private final Handler main = new Handler(Looper.getMainLooper()); + private final ExecutorService network = Executors.newSingleThreadExecutor(); + + private SensorManager sensorManager; + private TextView brokerText; + private TextView sensorText; + private TextView touchText; + private long touchSequence; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); + setContentView(buildUi()); + startSensors(); + pollBroker(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (sensorManager != null) { + sensorManager.unregisterListener(this); + } + network.shutdownNow(); + } + + private ScrollView buildUi() { + ScrollView scroll = new ScrollView(this); + scroll.setFillViewport(true); + scroll.setBackgroundColor(Color.rgb(2, 9, 9)); + + LinearLayout root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(dp(18), dp(18), dp(18), dp(18)); + scroll.addView(root, new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + TextView title = label("EVE / PERIWINKLE", 26, 0xff8efcff, true); + root.addView(title); + root.addView(label("CultMesh streaming UI client\nAndroid native proof", 14, 0xffb7c7c7, false)); + + brokerText = card("CultNet broker\nconnecting to Mimir at 192.168.1.66:8795"); + sensorText = card("timestamped sensors\nwaiting for accelerometer / gyro"); + touchText = card("touch surface\nwaiting for operator input"); + root.addView(brokerText); + root.addView(sensorText); + root.addView(touchText); + + TextView contract = card("contract\nBrowser is the reference renderer. Periwinkle renders the same Eve surface documents natively and publishes timestamped local signals back into the mesh."); + root.addView(contract); + return scroll; + } + + private TextView label(String text, int sp, int color, boolean title) { + TextView view = new TextView(this); + view.setText(text); + view.setTextSize(sp); + view.setTextColor(color); + view.setGravity(Gravity.START); + view.setIncludeFontPadding(true); + view.setTypeface(android.graphics.Typeface.MONOSPACE, title ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL); + return view; + } + + private TextView card(String text) { + TextView view = label(text, 14, 0xffe6f1f1, false); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, dp(14), 0, 0); + view.setLayoutParams(params); + view.setPadding(dp(14), dp(12), dp(14), dp(12)); + view.setBackgroundColor(Color.rgb(7, 25, 24)); + return view; + } + + private void startSensors() { + if (sensorManager == null) { + sensorText.setText("timestamped sensors\nSensorManager unavailable"); + return; + } + Sensor accel = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + Sensor gyro = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (accel != null) { + sensorManager.registerListener(this, accel, SensorManager.SENSOR_DELAY_GAME); + } + if (gyro != null) { + sensorManager.registerListener(this, gyro, SensorManager.SENSOR_DELAY_GAME); + } + } + + private void pollBroker() { + network.execute(new Runnable() { + @Override + public void run() { + String text; + try { + URL url = new URL("http://192.168.1.66:8795/health"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(1200); + connection.setReadTimeout(1200); + int status = connection.getResponseCode(); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder body = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + body.append(line); + } + text = "CultNet broker\nHTTP " + status + "\n" + body; + } catch (Exception ex) { + text = "CultNet broker\nwaiting for Mimir\n" + ex.getClass().getSimpleName() + ": " + ex.getMessage(); + } + final String finalText = text; + main.post(new Runnable() { + @Override + public void run() { + brokerText.setText(finalText); + } + }); + main.postDelayed(new Runnable() { + @Override + public void run() { + pollBroker(); + } + }, 2000); + } + }); + } + + @Override + public void onSensorChanged(SensorEvent event) { + long elapsedNs = SystemClock.elapsedRealtimeNanos(); + String name = event.sensor.getName(); + sensorText.setText(String.format(Locale.US, + "timestamped sensors\n%s\nsensorTs=%d\nelapsedNs=%d\nx=%+.3f y=%+.3f z=%+.3f", + name, + event.timestamp, + elapsedNs, + event.values.length > 0 ? event.values[0] : 0.0f, + event.values.length > 1 ? event.values[1] : 0.0f, + event.values.length > 2 ? event.values[2] : 0.0f)); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + touchSequence++; + touchText.setText(String.format(Locale.US, + "touch surface\nseq=%d action=%d pointers=%d eventTime=%d x=%.1f y=%.1f", + touchSequence, + event.getActionMasked(), + event.getPointerCount(), + event.getEventTime(), + event.getX(), + event.getY())); + return super.dispatchTouchEvent(event); + } + + private int dp(int value) { + return (int) (value * getResources().getDisplayMetrics().density + 0.5f); + } +} diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..dbd6444 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Eve + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..3c837c6 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + diff --git a/docs/cultmesh-streaming-ui-framework.md b/docs/cultmesh-streaming-ui-framework.md index 2175779..b02f6f3 100644 --- a/docs/cultmesh-streaming-ui-framework.md +++ b/docs/cultmesh-streaming-ui-framework.md @@ -85,6 +85,15 @@ Android: - camera, mic, motion, and display timing publisher; - native rendering where browser embedding would be weaker. +Flutter: + +- likely shared native-client implementation path for Android/iOS parity; +- useful for matching controls, layout primitives, animation, and local media + surfaces across native devices; +- not the browser ground truth. Flutter Web may be a compatibility target, but + the canonical behavior still belongs to the browser Eve runtime and recorded + CultMesh surface fixtures. + ## Current Proof The current iOS app consumes Mimir's `/eve/deck` WebSocket broker. That broker is @@ -94,6 +103,12 @@ provider proves that Eve can render an app-specific native cockpit from a structured mesh snapshot: CTB rail, avatar images, selected Face/status pane, state tree, and detail panel. +The Android proof under `android/` builds directly against the installed Android +SDK and runs on Periwinkle. It is deliberately small: display Eve's role, poll +the Mimir broker health endpoint, and show timestamped motion/touch samples. +It is a device-edge proof while the browser reference and Flutter/native shared +client are still being cut. + ## Next Cut 1. Extract the shared surface schema from the iOS app and Mimir broker into a diff --git a/scripts/build-android.ps1 b/scripts/build-android.ps1 new file mode 100644 index 0000000..2a579e2 --- /dev/null +++ b/scripts/build-android.ps1 @@ -0,0 +1,105 @@ +param( + [string] $AndroidSdk = "$env:LOCALAPPDATA\Android\Sdk", + [string] $JavaHome = "C:\Program Files\Android\Android Studio\jbr", + [string] $Configuration = "debug" +) + +$ErrorActionPreference = "Stop" +$env:JAVA_HOME = $JavaHome +$env:PATH = "$(Join-Path $JavaHome 'bin');$env:PATH" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$androidRoot = Join-Path $projectRoot "android\app" +$outRoot = Join-Path $projectRoot "artifacts\android" +$packageName = "org.gamecult.eve" +$buildTools = Get-ChildItem (Join-Path $AndroidSdk "build-tools") -Directory | Sort-Object Name -Descending | Select-Object -First 1 +$platform = Get-ChildItem (Join-Path $AndroidSdk "platforms") -Directory | Sort-Object Name -Descending | Select-Object -First 1 + +if (-not $buildTools -or -not $platform) { + throw "Android SDK build-tools/platforms not found under $AndroidSdk" +} + +$aapt2 = Join-Path $buildTools.FullName "aapt2.exe" +$d8 = Join-Path $buildTools.FullName "d8.bat" +$zipalign = Join-Path $buildTools.FullName "zipalign.exe" +$apksigner = Join-Path $buildTools.FullName "apksigner.bat" +$javac = Join-Path $JavaHome "bin\javac.exe" +$keytool = Join-Path $JavaHome "bin\keytool.exe" +$jar = Join-Path $JavaHome "bin\jar.exe" +$androidJar = Join-Path $platform.FullName "android.jar" + +foreach ($tool in @($aapt2, $d8, $zipalign, $apksigner, $javac, $keytool, $jar, $androidJar)) { + if (-not (Test-Path $tool)) { + throw "Required Android build input missing: $tool" + } +} + +function Invoke-Checked { + param( + [string] $FilePath, + [string[]] $Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$FilePath failed with exit code $LASTEXITCODE" + } +} + +New-Item -ItemType Directory -Force $outRoot | Out-Null +$compiled = Join-Path $outRoot "compiled.zip" +$gen = Join-Path $outRoot "gen" +$classes = Join-Path $outRoot "classes" +$classesJar = Join-Path $outRoot "classes.jar" +$dex = Join-Path $outRoot "dex" +$unsigned = Join-Path $outRoot "eve-unsigned.apk" +$aligned = Join-Path $outRoot "eve-aligned.apk" +$signed = Join-Path $outRoot "eve-$Configuration.apk" +$keystore = Join-Path $outRoot "eve-debug.keystore" + +Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $compiled, $gen, $classes, $classesJar, $dex, $unsigned, $aligned, $signed +New-Item -ItemType Directory -Force $gen, $classes, $dex | Out-Null + +Invoke-Checked $aapt2 @("compile", "--dir", (Join-Path $androidRoot "src\main\res"), "-o", $compiled) +Invoke-Checked $aapt2 @( + "link", + "-o", $unsigned, + "-I", $androidJar, + "--manifest", (Join-Path $androidRoot "src\main\AndroidManifest.xml"), + "--java", $gen, + "--min-sdk-version", "26", + "--target-sdk-version", "35", + $compiled) + +$sources = @() +$sources += Get-ChildItem (Join-Path $androidRoot "src\main\java") -Recurse -Filter *.java | Select-Object -ExpandProperty FullName +$sources += Get-ChildItem $gen -Recurse -Filter *.java | Select-Object -ExpandProperty FullName +Invoke-Checked $javac (@("-source", "8", "-target", "8", "-bootclasspath", $androidJar, "-d", $classes) + $sources) +Invoke-Checked $jar @("cf", $classesJar, "-C", $classes, ".") +Invoke-Checked $d8 @("--lib", $androidJar, "--output", $dex, $classesJar) +Invoke-Checked $jar @("uf", $unsigned, "-C", $dex, "classes.dex") +Invoke-Checked $zipalign @("-f", "4", $unsigned, $aligned) + +if (-not (Test-Path $keystore)) { + Invoke-Checked $keytool @( + "-genkeypair", "-v", + "-keystore", $keystore, + "-storepass", "android", + "-keypass", "android", + "-alias", "eve-debug", + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", "10000", + "-dname", "CN=Eve Debug,O=GameCult,C=US") +} + +Invoke-Checked $apksigner @( + "sign", + "--ks", $keystore, + "--ks-pass", "pass:android", + "--key-pass", "pass:android", + "--out", $signed, + $aligned) + +Invoke-Checked $apksigner @("verify", $signed) +Write-Host "Built $signed"