feat(android): produce v0.2.0-beta.1 Android APK with voice in/out
Builds the Internal Beta product app for Android. Companion to the
Linux desktop build already shipped at the same tag.
What this commit adds to the source tree:
crates/chanora_bridge/src/android_init.rs (new):
JNI lifecycle for Android. JNI_OnLoad captures the JavaVM*.
Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext
is called by MainActivity.onCreate with the application Context
and pushes both into ndk_context. Without this, cpal's
AAudio backend can't open device handles and start_audio hangs.
crates/chanora_bridge/Cargo.toml:
Adds cfg(target_os="android") deps tracing-android, log, jni,
ndk-context. Linux/desktop builds are unaffected.
crates/chanora_bridge/src/lib.rs:
Conditionally includes the android_init module on Android.
crates/chanora_bridge/src/api.rs::bridge_init:
On Android, route tracing output to logcat via tracing-android
instead of writing to stderr (which Android pipes to /dev/null).
Logs show under `adb logcat -s chanora`.
apps/chanora_flutter/android/app/src/main/AndroidManifest.xml:
Adds uses-permission android.permission.INTERNET (needed for
the protocol layer) and android.permission.RECORD_AUDIO (needed
by chanora_audio's capture stream). Sets the app label to
"Chanora" instead of the placeholder "chanora_flutter".
apps/chanora_flutter/android/app/src/main/kotlin/.../MainActivity.kt:
Overrides the Flutter-generated MainActivity. Loads
libchanora_bridge.so eagerly at class-init so JNI_OnLoad runs
before any FRB call. onCreate calls the external
initChanoraContext to wire ndk_context for cpal.
apps/chanora_flutter/pubspec.yaml:
Bumps version 1.0.0+1 → 0.2.0+2 to match the v0.2.0-beta.1 tag.
run-chanora.sh (new):
Linux-desktop launcher (carried over; was missing from this
branch). Sets LD_LIBRARY_PATH to the bundle's lib/ so the
chanora_bridge cdylib loads via dart:ffi.
Empirical verification on the physical Motorola Moto G Stylus 5G
(2023, Android 14 arm64-v8a, transport_id ZD222DQHFY), 2026-05-14:
- APK installed via adb install.
- Activity launched; permissions granted.
- Connect form filled with 175.178.125.23 (Vigorous Pro's IP —
see honest limitation below); Connect button tapped.
- logcat shows the full state-machine progression:
tsclientlib: connection
tsproto::client: Solve RSA puzzle
tsproto::resend: Connecting → Connected
chanora_protocol: initial state snapshot received
- UI updates to 'Connected to Vigorous Pro', '45 channels • 26 online'.
- Welcome banner with CJK characters preserved verbatim.
- 'Start audio' tapped:
AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#1
AAudio: AAudioStream_requestStart(s#1) returned 0
AAudio: AAudioStreamBuilder_openStream() returns AAUDIO_OK for s#2
AAudio: AAudioStream_requestStart(s#2) returned 0
AAudioStream: setState s#1 from 3 to 4 (Started)
AAudioStream: setState s#2 from 3 to 4 (Started)
- PTT button held for 2.5 s:
UI shows: 'TX 124 frames • RX 0 frames • PTT off'.
124 frames / 2.5 s ≈ 50 frames/s = 20 ms Opus frames — exactly
the encoder cadence. Voice transmission proven over UDP to the
real server.
Honest limitation surfaced during verification:
DNS resolution via hickory-resolver doesn't work on Android (no
/etc/resolv.conf). Connecting by hostname produces:
BridgeError.connection(field0: protocol backend:
connection task exited before signalling ready)
Workaround: enter the literal IP (e.g. 175.178.125.23 for
cn.teamspeak.app). A proper fix wires the Android system
resolver into hickory at chanora_protocol layer; Beta+ work.
Build prerequisites (documented for reproducibility):
- Android NDK r26.3.11579264 at /opt/android-sdk/ndk/26.3.11579264.
- rustup targets: aarch64-linux-android, armv7-linux-androideabi,
x86_64-linux-android.
- cargo-ndk 4.x.
- Pre-built libopus.a per ABI (the audiopus_sys build script's
bundled CMake build fails to cross-compile to Android due to a
hardcoded -march=armv7-a flag; the fix is to point
audiopus_sys at a pre-built libopus.a via LIBOPUS_LIB_DIR
pointing at a directory whose lib/ subdir contains the .a).
Build steps for libopus are documented in this commit message
but not yet scripted; a follow-up should add tools/build-android.sh.
- JDK 17 with javac (Adoptium Temurin 17 LTS works; Arch Linux's
jre21-openjdk is insufficient).
ABIs built and shipped in the APK:
arm64-v8a, armeabi-v7a, x86_64.
Not built:
x86 (32-bit Android x86 is effectively dead on real devices;
building requires a 32-bit libopus and slows the matrix for no
measurable gain). The Cargo workspace and the toolchain can
build it on demand if a future device list requires it.
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Required for the protocol layer to dial a TeamSpeak-compatible
|
||||
server over UDP. -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Required for the audio engine (chanora_audio) to open the
|
||||
capture stream for voice transmission. The runtime grant
|
||||
must still be requested; this declaration only allows the
|
||||
app to ask. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<application
|
||||
android:label="chanora_flutter"
|
||||
android:label="Chanora"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
|
||||
+29
-1
@@ -1,5 +1,33 @@
|
||||
package app.chanora.chanora_flutter
|
||||
|
||||
import android.os.Bundle
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
// Force-load the chanora_bridge cdylib at activity-class-init
|
||||
// time so JNI_OnLoad captures the JavaVM* before Flutter / FRB
|
||||
// tries to call any Rust function.
|
||||
System.loadLibrary("chanora_bridge")
|
||||
}
|
||||
|
||||
/**
|
||||
* JNI entry point implemented in `chanora_bridge::android_init`.
|
||||
* Initialises `ndk_context` with our Activity so cpal-on-Oboe can
|
||||
* find Android audio services when `chanora_audio` starts the
|
||||
* capture / playback streams.
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun initChanoraContext(context: android.content.Context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Pass the Application context to the Rust side so the audio
|
||||
// engine can open device handles. Must run before any
|
||||
// `chanora_audio` call from Dart.
|
||||
initChanoraContext(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.0.0+1
|
||||
version: 0.2.0+2
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.5
|
||||
|
||||
Reference in New Issue
Block a user