feat(android): scaffold the native Android client (Rust-heavy JNI bridge)
apple / swift (push) Successful in 52s
ci / docs-site (push) Successful in 27s
android / android (push) Successful in 4m52s
ci / web (push) Successful in 26s
ci / bench (push) Successful in 1m33s
ci / rust (push) Successful in 6m56s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m54s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m29s
deb / build-publish (push) Successful in 6m46s
docker / deploy-docs (push) Successful in 22s

Rust-heavy client model (like punktfunk-client-linux): a new cdylib crate
crates/punktfunk-android links punktfunk-core and exposes the JNI seam;
Kotlin (clients/android) owns only the Android-framework surface. Kotlin can't
import the C header the way Swift can, so the bridge is written in Rust to reuse
the Linux client's orchestration rather than re-port it.

- crates/punktfunk-android: JNI bridge — abiVersion/coreVersion native-link
  proof + session connect/close handle; plane pumps stubbed for M4 stage 1.
- clients/android: Gradle project — :app (Compose) + :kit (Android library with
  a cargo-ndk Exec task -> jniLibs). AGP 9.2 / Gradle 9.4.1 / Kotlin 2.3.21 /
  Compose BOM 2026.05.01 / compileSdk 37 / targetSdk 36 / minSdk 31, shipping
  arm64-v8a + x86_64. Phone + TV (leanback) installable. README rewritten.
- .gitea/workflows/android.yml: CI mirroring apple.yml on a Linux runner.
- punktfunk-core: switch rcgen to the ring backend so the whole quic tree is
  aws-lc-free (smaller client .so, cmake-free cross-compile; a win for all targets).

Validated on this box: :app:assembleDebug -> APK with both ABIs; emulator
first-light renders the bridge linked (core ABI v2) with logcat confirmation;
clippy -D warnings + cargo fmt clean; core tests green on the ring backend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 01:37:46 +02:00
parent c9e90d4a59
commit 79217eb93d
24 changed files with 1040 additions and 15 deletions
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- punktfunk/1 QUIC/UDP data plane. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- mDNS discovery of _punktfunk._udp on the LAN (NsdManager). -->
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<!-- Enforced from Android 17 (SDK 37) for ALL local-network traffic incl. the QUIC socket.
Harmless to declare on earlier releases. -->
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
<!-- Mic uplink to the host's virtual microphone (requested at runtime). -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Gamepad rumble feedback. -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- We target phone + TV from day one: keep the app installable on TV (no touchscreen) and on
devices without a gamepad. -->
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<application
android:allowBackup="true"
android:icon="@android:drawable/sym_def_app_icon"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.PunktfunkAndroid">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|density|navigation"
android:theme="@style/Theme.PunktfunkAndroid">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- TV launcher entry. -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,59 @@
package io.unom.punktfunk
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.unom.punktfunk.kit.NativeBridge
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Cross the JNI bridge into libpunktfunk_android.so → punktfunk-core. A live ABI version is
// the scaffold's proof the whole native stack is wired (cargo-ndk → jniLibs → APK →
// System.loadLibrary → JNI → core). Logged so it's verifiable headlessly via logcat.
val abi = runCatching { NativeBridge.abiVersion() }.getOrDefault(-1)
val core = runCatching { NativeBridge.coreVersion() }.getOrDefault("?")
Log.i("punktfunk", "native bridge: core ABI v$abi, core $core")
enableEdgeToEdge()
setContent {
MaterialTheme(colorScheme = darkColorScheme()) {
Surface(modifier = Modifier.fillMaxSize()) {
ScaffoldScreen(abi, core)
}
}
}
}
}
@Composable
private fun ScaffoldScreen(abi: Int, core: String) {
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text("punktfunk", style = MaterialTheme.typography.headlineMedium)
Text("Android client — scaffold", style = MaterialTheme.typography.bodyMedium)
Text(
if (abi > 0) "✓ native bridge linked" else "✗ native bridge FAILED",
style = MaterialTheme.typography.titleMedium,
)
Text("core ABI v$abi · core $core", style = MaterialTheme.typography.bodySmall)
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">punktfunk</string>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- The Activity is pure Compose; this platform theme just provides a no-action-bar host.
Compose draws its own Material 3 surfaces over it. -->
<style name="Theme.PunktfunkAndroid" parent="android:Theme.Material.NoActionBar" />
</resources>