feat: multi-platform bug fixes, Android audio path, and build tooling
Flutter UI fixes: - Fix stale channel badge/speaker when moved by others (derive current channel from ownClientId instead of optimistic local state) - Fix Linux PTT via focused fallback key handler - Distinguish ServerQuery clients with terminal icon in client list - Reduce duplicate current-channel badge display - Prevent PTT key-bind save from permanently closing voice settings - Fix Linux GTK reopen-after-close (quit app on window destroy) - Fix focused PTT: consume key events, release held keys on disconnect/leave-channel/mode/backend changes, suppress stale errors Flutter Rust bridge: - Thread is_server_query flag through protocol→bridge→Dart - Add own_client_id to BridgeSnapshot DTO - Add log_file_path_str() for platform log path queries Rust protocol: - Add ServerQuery test coverage (query_client_type_maps_to_server_query_flag) - Split reqwest TLS: native-tls for desktop/iOS, rustls for Android Rust audio: - Upgrade cpal 0.16→0.17.3 with API adjustments (SampleRate, description()) - Suppress Android-only dead-code warnings (open_log_file, keyring_account) Android build tooling: - tools/build-opus-android.sh: NDK auto-discovery, correct CMake Android variables (ANDROID_ABI, ANDROID_PLATFORM), portable baseline - tools/build-android-rust.sh: build+copy Rust cdylib for arm64-v8a, armeabi-v7a, x86_64 into android/app/src/main/jniLibs/ - Add jniLibs/ to .gitignore Rust bridge: - Guard open_log_file() on non-Android (Android uses logcat)
This commit is contained in:
Generated
+369
-30
@@ -72,9 +72,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.11.1",
|
||||
@@ -84,9 +84,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
@@ -193,6 +193,28 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.41.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
@@ -256,6 +278,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-target"
|
||||
version = "0.4.0"
|
||||
@@ -293,6 +324,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
@@ -308,6 +341,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.9.1"
|
||||
@@ -349,7 +388,7 @@ version = "0.2.0-beta.1"
|
||||
dependencies = [
|
||||
"audiopus",
|
||||
"chanora_protocol",
|
||||
"coreaudio-rs 0.14.2",
|
||||
"coreaudio-rs",
|
||||
"cpal",
|
||||
"futures-util",
|
||||
"jni 0.21.1",
|
||||
@@ -362,7 +401,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tsclientlib",
|
||||
"windows",
|
||||
"windows 0.54.0",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -422,6 +461,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tsclientlib",
|
||||
"tsproto-packets",
|
||||
"tsproto-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -540,20 +580,6 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.2"
|
||||
@@ -570,12 +596,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.16.0"
|
||||
version = "0.17.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
|
||||
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs 0.13.0",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni 0.21.1",
|
||||
"js-sys",
|
||||
@@ -585,13 +611,17 @@ dependencies = [
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-avf-audio",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -846,6 +876,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "eax"
|
||||
version = "0.5.0"
|
||||
@@ -1095,6 +1131,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
@@ -1214,8 +1256,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1226,7 +1284,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
@@ -1784,6 +1842,16 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.98"
|
||||
@@ -1888,10 +1956,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -2145,6 +2219,16 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-avf-audio"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.2"
|
||||
@@ -2155,6 +2239,7 @@ dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2174,7 +2259,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
@@ -2190,7 +2277,11 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2475,6 +2566,62 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.4",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.18",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -2484,6 +2631,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
@@ -2497,10 +2650,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
@@ -2522,6 +2685,16 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -2531,6 +2704,15 @@ dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
@@ -2619,10 +2801,14 @@ dependencies = [
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
@@ -2682,6 +2868,12 @@ version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -2710,6 +2902,7 @@ version = "0.23.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"once_cell",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
@@ -2717,21 +2910,62 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework 3.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
@@ -3872,6 +4106,25 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
@@ -3893,10 +4146,31 @@ version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.54.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.62.2",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
@@ -3907,12 +4181,68 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core 0.62.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
@@ -4035,6 +4365,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
|
||||
@@ -12,3 +12,4 @@ GeneratedPluginRegistrant.java
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
/app/src/main/jniLibs/
|
||||
|
||||
+126
-140
@@ -32,6 +32,48 @@ bool get _isMacOS => !kIsWeb && Platform.isMacOS;
|
||||
/// Top padding for macOS to clear traffic-light buttons.
|
||||
const double _macOSTrafficLightPad = 56.0;
|
||||
|
||||
/// Translate a [LogicalKeyboardKey] into the platform-neutral label
|
||||
/// stored by the PTT binding flow.
|
||||
String? _pttDisplayLabelForKey(LogicalKeyboardKey k) {
|
||||
if (k == LogicalKeyboardKey.space) return 'Space';
|
||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||
return 'Enter';
|
||||
}
|
||||
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
||||
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
||||
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
||||
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
||||
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
||||
if (k == LogicalKeyboardKey.home) return 'Home';
|
||||
if (k == LogicalKeyboardKey.end) return 'End';
|
||||
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
||||
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
||||
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
||||
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
||||
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
||||
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
||||
if (k == LogicalKeyboardKey.shift ||
|
||||
k == LogicalKeyboardKey.shiftLeft ||
|
||||
k == LogicalKeyboardKey.shiftRight ||
|
||||
k == LogicalKeyboardKey.control ||
|
||||
k == LogicalKeyboardKey.controlLeft ||
|
||||
k == LogicalKeyboardKey.controlRight ||
|
||||
k == LogicalKeyboardKey.alt ||
|
||||
k == LogicalKeyboardKey.altLeft ||
|
||||
k == LogicalKeyboardKey.altRight ||
|
||||
k == LogicalKeyboardKey.meta ||
|
||||
k == LogicalKeyboardKey.metaLeft ||
|
||||
k == LogicalKeyboardKey.metaRight ||
|
||||
k == LogicalKeyboardKey.capsLock ||
|
||||
k == LogicalKeyboardKey.numLock ||
|
||||
k == LogicalKeyboardKey.scrollLock) {
|
||||
return null;
|
||||
}
|
||||
final fallback = k.keyLabel.trim();
|
||||
if (fallback.isEmpty) return null;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/// True when the host is a touch-only mobile platform without a
|
||||
/// hardware keyboard. Mirrors the helpers in widgets/voice_bar.dart
|
||||
/// and widgets/voice_settings.dart so the AppBar + narrow-mode
|
||||
@@ -241,17 +283,50 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
// only the platform-neutral label that already crossed into
|
||||
// the Rust side.
|
||||
String _pttBoundKeyLabel = '';
|
||||
final Set<LogicalKeyboardKey> _focusedPttHeldKeys = <LogicalKeyboardKey>{};
|
||||
|
||||
List<rust.BridgeBookmark> _bookmarks = const [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
HardwareKeyboard.instance.addHandler(_handleFocusedPttKey);
|
||||
_eventsSub = rust.eventsStream().listen(_onEvent);
|
||||
unawaited(_reloadBookmarks());
|
||||
unawaited(_hydratePttBinding());
|
||||
}
|
||||
|
||||
bool _handleFocusedPttKey(KeyEvent event) {
|
||||
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||
final isBoundKey = _pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
|
||||
if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) {
|
||||
if (_focusedPttHeldKeys.remove(event.logicalKey)) {
|
||||
unawaited(_setPtt(false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_pttBackendId != 'focused' ||
|
||||
_phase != _Phase.connected ||
|
||||
!_inChannel ||
|
||||
_transmitMode != rust.BridgeTransmitMode.ptt ||
|
||||
!isBoundKey) {
|
||||
return false;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
if (_focusedPttHeldKeys.add(event.logicalKey)) {
|
||||
unawaited(_setPtt(true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _releaseFocusedPttIfHeld() {
|
||||
if (_focusedPttHeldKeys.isEmpty) return;
|
||||
_focusedPttHeldKeys.clear();
|
||||
unawaited(_setPtt(false, reportError: false));
|
||||
}
|
||||
|
||||
/// Hydrate the bound-key display state from the bridge so the
|
||||
/// Voice Bar shows the user's persisted hotkey label immediately
|
||||
/// at launch — without having to wait for them to open the
|
||||
@@ -326,6 +401,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_pttBackendId = backendId;
|
||||
_pttBoundInputClass = boundInputClass;
|
||||
});
|
||||
if (backendId != 'focused') {
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
case rust.BridgeEvent_VoiceState(
|
||||
:final inChannel,
|
||||
:final transmitMode,
|
||||
@@ -341,9 +419,15 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
});
|
||||
if (inChannel) {
|
||||
_ensureStatsTimer();
|
||||
unawaited(_onRefresh());
|
||||
} else {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = null;
|
||||
_releaseFocusedPttIfHeld();
|
||||
setState(() => _currentVoiceChannelId = null);
|
||||
}
|
||||
if (transmitMode != rust.BridgeTransmitMode.ptt) {
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
case rust.BridgeEvent_InterruptionState(
|
||||
:final began,
|
||||
@@ -403,6 +487,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
|
||||
_eventsSub?.cancel();
|
||||
_statsTimer?.cancel();
|
||||
_hostCtl.dispose();
|
||||
@@ -421,6 +506,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_error = null;
|
||||
_snapshot = null;
|
||||
});
|
||||
_releaseFocusedPttIfHeld();
|
||||
try {
|
||||
final snap = await rust.connect(
|
||||
host: (host ?? _hostCtl.text).trim(),
|
||||
@@ -430,7 +516,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_phase = _Phase.connected;
|
||||
_snapshot = snap;
|
||||
_applySnapshot(snap);
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -483,11 +569,11 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _setPtt(bool active) async {
|
||||
Future<void> _setPtt(bool active, {bool reportError = true}) async {
|
||||
try {
|
||||
await rust.setPtt(active: active);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (!mounted || !reportError) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
@@ -719,6 +805,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
if (result.bindKeyRequested && mounted) {
|
||||
await _onConfigurePtt(context);
|
||||
if (mounted) {
|
||||
unawaited(_onOpenVoiceSettings());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,7 +830,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
try {
|
||||
final snap = await rust.snapshot();
|
||||
if (!mounted) return;
|
||||
setState(() => _snapshot = snap);
|
||||
setState(() => _applySnapshot(snap));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
@@ -766,6 +855,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_inChannel = false;
|
||||
_currentVoiceChannelId = null;
|
||||
});
|
||||
_releaseFocusedPttIfHeld();
|
||||
}
|
||||
|
||||
String _currentVoiceChannelName() {
|
||||
@@ -778,6 +868,21 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
return '';
|
||||
}
|
||||
|
||||
void _applySnapshot(rust.BridgeSnapshot snap) {
|
||||
_snapshot = snap;
|
||||
if (!_inChannel) {
|
||||
_currentVoiceChannelId = null;
|
||||
return;
|
||||
}
|
||||
for (final client in snap.clients) {
|
||||
if (client.id == snap.ownClientId) {
|
||||
_currentVoiceChannelId = client.channel;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_currentVoiceChannelId = null;
|
||||
}
|
||||
|
||||
Future<void> _onShowDiagnostics(BuildContext context) async {
|
||||
final l10n = AppL10n.of(context);
|
||||
final text = rust.exportDiagnostics();
|
||||
@@ -1045,11 +1150,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
],
|
||||
];
|
||||
|
||||
final headerTitle = _AppBarTitle(
|
||||
phase: _phase,
|
||||
inChannel: _inChannel,
|
||||
channelName: _currentVoiceChannelName(),
|
||||
);
|
||||
const headerTitle = _AppBarTitle();
|
||||
|
||||
final bodyContent = LayoutBuilder(
|
||||
builder: (ctx, bodyConstraints) {
|
||||
@@ -1260,81 +1361,16 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
/// AppBar title that shows just the app name when idle / connecting,
|
||||
/// and 'app name · #channel' (channel as an outlined chip-style
|
||||
/// pill) when the user is in a voice channel. The chip is read-
|
||||
/// only — tapping does nothing because the source of truth for
|
||||
/// "current channel" is the channel tree below. Long names ellipsize.
|
||||
/// App header title. The current channel is highlighted only in the
|
||||
/// channel tree and Voice Bar to avoid repeating the same badge in
|
||||
/// multiple places.
|
||||
class _AppBarTitle extends StatelessWidget {
|
||||
const _AppBarTitle({
|
||||
required this.phase,
|
||||
required this.inChannel,
|
||||
required this.channelName,
|
||||
});
|
||||
|
||||
final _Phase phase;
|
||||
final bool inChannel;
|
||||
final String channelName;
|
||||
const _AppBarTitle();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppL10n.of(context);
|
||||
final theme = Theme.of(context);
|
||||
if (phase != _Phase.connected || !inChannel || channelName.isEmpty) {
|
||||
return Text(l10n.appTitle);
|
||||
}
|
||||
// When in a voice channel on a narrow phone width, the AppBar
|
||||
// is already crowded with mic / headset / settings / about /
|
||||
// diagnostics / disconnect icons (5-6 action buttons). Keeping
|
||||
// the "Chanora" app-name label in the title here causes the
|
||||
// Row to overflow at typical iPhone widths and Flutter renders
|
||||
// its yellow-and-black "OVERFLOWED BY X PIXELS" debug strip
|
||||
// next to the title — the user reported seeing "RFLOWED BY"
|
||||
// there. Drop the app-name label on narrow widths and let the
|
||||
// channel pill be the only title content; the user knows
|
||||
// they're in Chanora because they just opened it. On wide
|
||||
// widths (>= 840 dp, tablet/desktop) restore the app name
|
||||
// because there's plenty of room.
|
||||
final isNarrow = MediaQuery.of(context).size.width < 840.0;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!isNarrow) ...[Text(l10n.appTitle), const SizedBox(width: 12)],
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.tag,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
channelName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return Text(l10n.appTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1995,8 +2031,16 @@ class _SnapshotView extends StatelessWidget {
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
visualDensity: VisualDensity.compact,
|
||||
leading: const Icon(Icons.person, size: 18),
|
||||
title: Text(cl.name),
|
||||
leading: Icon(
|
||||
cl.isServerQuery ? Icons.terminal : Icons.person,
|
||||
size: 18,
|
||||
),
|
||||
title: Text(
|
||||
cl.name,
|
||||
style: cl.isServerQuery
|
||||
? TextStyle(color: theme.colorScheme.onSurfaceVariant)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2049,7 +2093,7 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
||||
|
||||
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||
final label = _displayLabelForKey(event.logicalKey);
|
||||
final label = _pttDisplayLabelForKey(event.logicalKey);
|
||||
if (label == null) return KeyEventResult.ignored;
|
||||
setState(() {
|
||||
_captured = label;
|
||||
@@ -2058,64 +2102,6 @@ class _PttBindingCaptureDialogState extends State<_PttBindingCaptureDialog> {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
/// Translate a [LogicalKeyboardKey] into the platform-neutral
|
||||
/// label string the bridge expects (matching the entries in
|
||||
/// `crates/chanora_audio/src/ptt_backends/windows_keymap.rs`).
|
||||
///
|
||||
/// `LogicalKeyboardKey.keyLabel` returns `" "` for Space, empty
|
||||
/// for pure modifiers (shift / ctrl / alt / meta), and localised
|
||||
/// strings for some special keys; we normalise to the canonical
|
||||
/// English label so the badge displays something readable AND so
|
||||
/// the Windows backend's keymap can resolve to a `VK_*`. Pure
|
||||
/// modifier keys are intentionally rejected — chording into the
|
||||
/// real binding (e.g. Ctrl+Shift+M) is supported by ignoring the
|
||||
/// individual modifier-down events.
|
||||
String? _displayLabelForKey(LogicalKeyboardKey k) {
|
||||
// Whitespace / common control keys whose keyLabel is unhelpful.
|
||||
if (k == LogicalKeyboardKey.space) return 'Space';
|
||||
if (k == LogicalKeyboardKey.enter || k == LogicalKeyboardKey.numpadEnter) {
|
||||
return 'Enter';
|
||||
}
|
||||
if (k == LogicalKeyboardKey.tab) return 'Tab';
|
||||
if (k == LogicalKeyboardKey.escape) return 'Escape';
|
||||
if (k == LogicalKeyboardKey.backspace) return 'Backspace';
|
||||
if (k == LogicalKeyboardKey.delete) return 'Delete';
|
||||
if (k == LogicalKeyboardKey.insert) return 'Insert';
|
||||
if (k == LogicalKeyboardKey.home) return 'Home';
|
||||
if (k == LogicalKeyboardKey.end) return 'End';
|
||||
if (k == LogicalKeyboardKey.pageUp) return 'Page Up';
|
||||
if (k == LogicalKeyboardKey.pageDown) return 'Page Down';
|
||||
if (k == LogicalKeyboardKey.arrowUp) return 'Arrow Up';
|
||||
if (k == LogicalKeyboardKey.arrowDown) return 'Arrow Down';
|
||||
if (k == LogicalKeyboardKey.arrowLeft) return 'Arrow Left';
|
||||
if (k == LogicalKeyboardKey.arrowRight) return 'Arrow Right';
|
||||
// Pure modifier keys are not bindable on their own (user can
|
||||
// still chord by pressing a non-modifier while holding them).
|
||||
if (k == LogicalKeyboardKey.shift ||
|
||||
k == LogicalKeyboardKey.shiftLeft ||
|
||||
k == LogicalKeyboardKey.shiftRight ||
|
||||
k == LogicalKeyboardKey.control ||
|
||||
k == LogicalKeyboardKey.controlLeft ||
|
||||
k == LogicalKeyboardKey.controlRight ||
|
||||
k == LogicalKeyboardKey.alt ||
|
||||
k == LogicalKeyboardKey.altLeft ||
|
||||
k == LogicalKeyboardKey.altRight ||
|
||||
k == LogicalKeyboardKey.meta ||
|
||||
k == LogicalKeyboardKey.metaLeft ||
|
||||
k == LogicalKeyboardKey.metaRight ||
|
||||
k == LogicalKeyboardKey.capsLock ||
|
||||
k == LogicalKeyboardKey.numLock ||
|
||||
k == LogicalKeyboardKey.scrollLock) {
|
||||
return null;
|
||||
}
|
||||
// Fall back to keyLabel for letters, digits, function keys,
|
||||
// numpad digits, and punctuation. Trim whitespace as a final
|
||||
// belt-and-braces guard.
|
||||
final fallback = k.keyLabel.trim();
|
||||
if (fallback.isEmpty) return null;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _captureMouseSideButton(int button) {
|
||||
setState(() {
|
||||
_captured = 'mouse-side-button:$button';
|
||||
|
||||
@@ -338,14 +338,19 @@ class BridgeClient {
|
||||
/// Nickname.
|
||||
final String name;
|
||||
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
final bool isServerQuery;
|
||||
|
||||
const BridgeClient({
|
||||
required this.id,
|
||||
required this.channel,
|
||||
required this.name,
|
||||
required this.isServerQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ channel.hashCode ^ name.hashCode;
|
||||
int get hashCode =>
|
||||
id.hashCode ^ channel.hashCode ^ name.hashCode ^ isServerQuery.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -354,7 +359,8 @@ class BridgeClient {
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id &&
|
||||
channel == other.channel &&
|
||||
name == other.name;
|
||||
name == other.name &&
|
||||
isServerQuery == other.isServerQuery;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -1157,12 +1157,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
BridgeClient dco_decode_bridge_client(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 3)
|
||||
throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
return BridgeClient(
|
||||
id: dco_decode_u_64(arr[0]),
|
||||
channel: dco_decode_u_64(arr[1]),
|
||||
name: dco_decode_String(arr[2]),
|
||||
isServerQuery: dco_decode_bool(arr[3]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1454,7 +1455,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
var var_id = sse_decode_u_64(deserializer);
|
||||
var var_channel = sse_decode_u_64(deserializer);
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
return BridgeClient(id: var_id, channel: var_channel, name: var_name);
|
||||
var var_isServerQuery = sse_decode_bool(deserializer);
|
||||
return BridgeClient(
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
isServerQuery: var_isServerQuery,
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1799,6 +1806,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_u_64(self.id, serializer);
|
||||
sse_encode_u_64(self.channel, serializer);
|
||||
sse_encode_String(self.name, serializer);
|
||||
sse_encode_bool(self.isServerQuery, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
|
||||
@@ -19,6 +19,10 @@ static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
static void window_destroy_cb(GtkWidget* widget, gpointer user_data) {
|
||||
g_application_quit(G_APPLICATION(user_data));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
@@ -45,13 +49,15 @@ static void my_application_activate(GApplication* application) {
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "chanora_flutter");
|
||||
gtk_header_bar_set_title(header_bar, "Chanora");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "chanora_flutter");
|
||||
gtk_window_set_title(window, "Chanora");
|
||||
}
|
||||
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(window_destroy_cb), application);
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
|
||||
@@ -1735,6 +1735,7 @@ mod tests {
|
||||
id: chanora_protocol::ClientId(10),
|
||||
channel: chanora_protocol::ChannelId(1),
|
||||
name: "u".into(),
|
||||
is_server_query: false,
|
||||
}],
|
||||
own_client_id: 10,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
# Cross-platform audio I/O (DEC-011.1).
|
||||
cpal = "0.16"
|
||||
cpal = "0.17.3"
|
||||
# Opus encoder. tsclientlib already pulls this; we depend explicitly so
|
||||
# this crate can compile against it without going through tsclientlib.
|
||||
audiopus = "0.3.0-rc.0"
|
||||
@@ -24,19 +24,13 @@ audiopus = "0.3.0-rc.0"
|
||||
# feature. We import the crate just for the AudioHandler type; the
|
||||
# Connection type stays inside chanora_protocol.
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
||||
# answer for "TLS that just works on every desktop+mobile platform
|
||||
# without a vendored C dependency". Cargo's feature unification means
|
||||
# this single direct dep applies to the transitive
|
||||
# tsclientlib -> reqwest chain too.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
# Direct CoreAudio AudioUnit access on iOS (DEC-011.x follow-up).
|
||||
# cpal's iOS backend is unsuitable for VoIP: it opens
|
||||
@@ -59,6 +53,9 @@ tokio = { version = "1", features = ["sync", "rt", "macros", "time"] }
|
||||
coreaudio-rs = "0.14"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||
# JNI bindings to flip Android's AudioManager into MODE_IN_COMMUNICATION
|
||||
# when the voice-comm preset is requested. ndk_context is initialised
|
||||
# by the bridge crate's android_init shim.
|
||||
|
||||
@@ -251,8 +251,8 @@ impl AudioEngine {
|
||||
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
in_device = %in_dev.name().unwrap_or_default(),
|
||||
out_device = %out_dev.name().unwrap_or_default(),
|
||||
in_device = %in_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
out_device = %out_dev.description().map(|d| d.name().to_owned()).unwrap_or_default(),
|
||||
"starting audio engine"
|
||||
);
|
||||
|
||||
@@ -270,7 +270,7 @@ impl AudioEngine {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_input_config reported"
|
||||
),
|
||||
@@ -284,7 +284,7 @@ impl AudioEngine {
|
||||
Ok(c) => info!(
|
||||
target: "chanora_audio",
|
||||
channels = c.channels(),
|
||||
sample_rate = c.sample_rate().0,
|
||||
sample_rate = c.sample_rate(),
|
||||
sample_format = ?c.sample_format(),
|
||||
"default_output_config reported"
|
||||
),
|
||||
@@ -409,7 +409,7 @@ impl AudioEngine {
|
||||
.default_output_config()
|
||||
.map_err(|e| AudioError::StreamConfig(format!("output default: {e}")))?;
|
||||
let out_format = out_cfg.sample_format();
|
||||
let dev_sample_rate = out_cfg.sample_rate().0;
|
||||
let dev_sample_rate = out_cfg.sample_rate();
|
||||
let dev_channels = out_cfg.channels() as usize;
|
||||
// Buffer-size rationale:
|
||||
// * Windows (WASAPI via cpal): the default period is
|
||||
@@ -840,7 +840,7 @@ fn try_open_capture(
|
||||
let in_cfg = in_dev
|
||||
.default_input_config()
|
||||
.map_err(|e| AudioError::StreamConfig(format!("input default: {e}")))?;
|
||||
let in_sample_rate = in_cfg.sample_rate().0;
|
||||
let in_sample_rate = in_cfg.sample_rate();
|
||||
let in_channels = in_cfg.channels() as usize;
|
||||
let in_format = in_cfg.sample_format();
|
||||
// Buffer-size rationale (same shape as the output path):
|
||||
@@ -1339,7 +1339,7 @@ where
|
||||
target: "chanora_audio",
|
||||
error = %e,
|
||||
requested_channels = config.channels,
|
||||
requested_sample_rate = config.sample_rate.0,
|
||||
requested_sample_rate = config.sample_rate,
|
||||
"build_output_stream FAILED"
|
||||
);
|
||||
AudioError::Backend(format!("build_output_stream: {e}"))
|
||||
|
||||
@@ -217,6 +217,7 @@ fn log_file_path() -> Option<std::path::PathBuf> {
|
||||
/// Open the platform log file in append mode, rotating once on
|
||||
/// startup so each launch begins with a fresh file. Best-effort:
|
||||
/// returns `None` on any I/O error.
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn open_log_file() -> Option<std::fs::File> {
|
||||
let path = log_file_path()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
@@ -278,6 +279,8 @@ pub struct BridgeClient {
|
||||
pub channel: u64,
|
||||
/// Nickname.
|
||||
pub name: String,
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
pub is_server_query: bool,
|
||||
}
|
||||
|
||||
/// Server snapshot as seen by Dart.
|
||||
@@ -325,6 +328,7 @@ impl From<chanora_protocol::ServerSnapshot> for BridgeSnapshot {
|
||||
id: c.id.0,
|
||||
channel: c.channel.0,
|
||||
name: c.name,
|
||||
is_server_query: c.is_server_query,
|
||||
})
|
||||
.collect(),
|
||||
own_client_id: s.own_client_id,
|
||||
|
||||
@@ -1290,10 +1290,12 @@ impl SseDecode for crate::api::BridgeClient {
|
||||
let mut var_id = <u64>::sse_decode(deserializer);
|
||||
let mut var_channel = <u64>::sse_decode(deserializer);
|
||||
let mut var_name = <String>::sse_decode(deserializer);
|
||||
let mut var_isServerQuery = <bool>::sse_decode(deserializer);
|
||||
return crate::api::BridgeClient {
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
is_server_query: var_isServerQuery,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1721,6 +1723,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::BridgeClient {
|
||||
self.id.into_into_dart().into_dart(),
|
||||
self.channel.into_into_dart().into_dart(),
|
||||
self.name.into_into_dart().into_dart(),
|
||||
self.is_server_query.into_into_dart().into_dart(),
|
||||
]
|
||||
.into_dart()
|
||||
}
|
||||
@@ -1999,6 +2002,7 @@ impl SseEncode for crate::api::BridgeClient {
|
||||
<u64>::sse_encode(self.id, serializer);
|
||||
<u64>::sse_encode(self.channel, serializer);
|
||||
<String>::sse_encode(self.name, serializer);
|
||||
<bool>::sse_encode(self.is_server_query, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,23 +19,23 @@ once_cell = "1"
|
||||
# pulls in `audiopus` only — `sdl2` is a dev-dep used by upstream
|
||||
# examples; the library itself does not link SDL.
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491", default-features = false, features = ["audio"] }
|
||||
# Force the reqwest TLS backend to native-tls (Security.framework on
|
||||
# Apple, SChannel on Windows, system OpenSSL on Linux/BSD) instead of
|
||||
# the rustls + aws-lc-rs combination tsclientlib's `default-tls`
|
||||
# feature would otherwise pick. aws-lc-sys does not cross-compile
|
||||
# cleanly to aarch64-apple-ios, and native-tls is the standard
|
||||
# answer for "TLS that just works on every desktop+mobile platform
|
||||
# without a vendored C dependency". Cargo's feature unification means
|
||||
# this single direct dep applies to the transitive
|
||||
# tsclientlib -> reqwest chain too.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
# tsproto_packets exposes OutAudio / InAudioBuf / AudioData /
|
||||
# CodecType / Direction. Pinning to the same git rev as tsclientlib
|
||||
# avoids any version-skew confusion.
|
||||
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||
tsproto-types = { git = "https://github.com/ReSpeak/tsclientlib.git", rev = "04aa2491" }
|
||||
|
||||
# Async runtime utilities used by the connection task.
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
|
||||
futures = "0.3"
|
||||
async-trait = "0.1"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
# Desktop/iOS: native TLS maps to the platform TLS backend (Security.framework
|
||||
# on Apple, SChannel on Windows, system OpenSSL on Linux/BSD).
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "native-tls"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
# Android cross-builds should not pull OpenSSL. Use rustls here while keeping
|
||||
# native-tls for Apple targets where aws-lc/rustls is problematic for iOS.
|
||||
reqwest = { version = "0.13", default-features = false, features = ["charset", "http2", "rustls"] }
|
||||
|
||||
@@ -31,6 +31,7 @@ use tsclientlib::{
|
||||
OutCommandExt, StreamItem, Version,
|
||||
};
|
||||
use tsproto_packets::packets::{InAudioBuf, OutPacket};
|
||||
use tsproto_types::ClientType;
|
||||
|
||||
use crate::dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot};
|
||||
use crate::ProtocolError;
|
||||
@@ -872,6 +873,7 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
||||
id: ClientId(c.id.0 as u64),
|
||||
channel: ChannelId(c.channel.0),
|
||||
name: sanitize(&c.name),
|
||||
is_server_query: is_server_query_client_type(&c.client_type),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -886,6 +888,10 @@ fn build_snapshot(con: &Connection) -> Result<ServerSnapshot, ProtocolError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_server_query_client_type(client_type: &ClientType) -> bool {
|
||||
matches!(client_type, ClientType::Query { .. })
|
||||
}
|
||||
|
||||
/// Light sanitisation of strings before they cross the protocol
|
||||
/// boundary. The redaction policy proper lives in
|
||||
/// `chanora_diagnostics`; this filter only strips control characters
|
||||
@@ -905,7 +911,8 @@ const _ROOT_MATCHES_UPSTREAM: () = {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::sort_channels_tree_by;
|
||||
use super::{is_server_query_client_type, sort_channels_tree_by};
|
||||
use tsproto_types::ClientType;
|
||||
|
||||
/// Lightweight fixture mirroring just the (id, parent, order)
|
||||
/// triple that the linked-list sort needs. Avoids constructing
|
||||
@@ -922,6 +929,13 @@ mod tests {
|
||||
(c.id, c.parent, c.order)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_client_type_maps_to_server_query_flag() {
|
||||
assert!(!is_server_query_client_type(&ClientType::Normal));
|
||||
assert!(is_server_query_client_type(&ClientType::Query { admin: false }));
|
||||
assert!(is_server_query_client_type(&ClientType::Query { admin: true }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_sort_linked_list_under_one_parent() {
|
||||
// Server emits four root-level channels in arbitrary HashMap
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct ClientInfo {
|
||||
pub channel: ChannelId,
|
||||
/// Nickname, preserved verbatim per ADR-008.
|
||||
pub name: String,
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
pub is_server_query: bool,
|
||||
}
|
||||
|
||||
/// Snapshot of the server's published state at a moment in time.
|
||||
|
||||
@@ -173,6 +173,7 @@ pub struct IdentityFileStore {
|
||||
/// Stable per-install identifier used as the keyring account
|
||||
/// name. Derived from the install directory so the same store
|
||||
/// finds the same keyring entry across restarts.
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
keyring_account: String,
|
||||
}
|
||||
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
REPO_ROOT="$(pwd)"
|
||||
|
||||
NDK_ROOT="${ANDROID_NDK_HOME:-}"
|
||||
if [[ -z "$NDK_ROOT" && -n "${ANDROID_HOME:-}" ]]; then
|
||||
NDK_ROOT="$(find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||
fi
|
||||
if [[ -z "$NDK_ROOT" && -n "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||
NDK_ROOT="$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||
fi
|
||||
|
||||
ANDROID_PLATFORM=28
|
||||
JNI_LIBS_DIR="$REPO_ROOT/apps/chanora_flutter/android/app/src/main/jniLibs"
|
||||
|
||||
declare -a BUILDS=(
|
||||
"arm64-v8a;aarch64-linux-android;/tmp/opus-android-arm64-v8a"
|
||||
"armeabi-v7a;armv7-linux-androideabi;/tmp/opus-android-armeabi-v7a"
|
||||
"x86_64;x86_64-linux-android;/tmp/opus-android-x86_64"
|
||||
)
|
||||
|
||||
bar() { printf '%s\n' '========================================================================'; }
|
||||
|
||||
require_cmd() {
|
||||
local cmd=$1
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: '$cmd' is required but not on PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
bar
|
||||
echo "Chanora Android Rust build"
|
||||
echo "Repo root : $REPO_ROOT"
|
||||
echo "NDK : $NDK_ROOT"
|
||||
echo "Android platform: $ANDROID_PLATFORM"
|
||||
bar
|
||||
|
||||
require_cmd cargo
|
||||
require_cmd cargo-ndk
|
||||
|
||||
if [[ ! -d "$NDK_ROOT" ]]; then
|
||||
echo "ERROR: Android NDK not found. Set ANDROID_NDK_HOME or ANDROID_HOME." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export ANDROID_NDK_HOME="$NDK_ROOT"
|
||||
export LIBOPUS_STATIC=1
|
||||
export LIBOPUS_NO_PKG=1
|
||||
|
||||
for build in "${BUILDS[@]}"; do
|
||||
IFS=';' read -r abi triple opus_dir <<< "$build"
|
||||
if [[ ! -f "$opus_dir/lib/libopus.a" ]]; then
|
||||
echo "ERROR: missing $opus_dir/lib/libopus.a" >&2
|
||||
echo "Run tools/build-opus-android.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building Rust bridge for $abi ($triple)"
|
||||
export LIBOPUS_LIB_DIR="$opus_dir"
|
||||
cargo ndk --platform "$ANDROID_PLATFORM" -t "$abi" build --release -p chanora_bridge
|
||||
|
||||
mkdir -p "$JNI_LIBS_DIR/$abi"
|
||||
cp "$REPO_ROOT/target/$triple/release/libchanora_bridge.so" "$JNI_LIBS_DIR/$abi/"
|
||||
done
|
||||
|
||||
bar
|
||||
echo "Android Rust libraries copied to $JNI_LIBS_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NDK_ROOT="${ANDROID_NDK_HOME:-}"
|
||||
if [[ -z "$NDK_ROOT" && -n "${ANDROID_HOME:-}" ]]; then
|
||||
NDK_ROOT="$(find "$ANDROID_HOME/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||
fi
|
||||
if [[ -z "$NDK_ROOT" && -n "${ANDROID_SDK_ROOT:-}" ]]; then
|
||||
NDK_ROOT="$(find "$ANDROID_SDK_ROOT/ndk" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort | tail -1 || true)"
|
||||
fi
|
||||
OPUS_VERSION="v1.3.1"
|
||||
OPUS_SRC_DIR="/tmp/opus-1.3.1"
|
||||
API_LEVEL=21
|
||||
|
||||
declare -a ABIS=(
|
||||
"arm64-v8a"
|
||||
"armeabi-v7a"
|
||||
"x86_64"
|
||||
)
|
||||
|
||||
bar() { printf '%s\n' '========================================================================'; }
|
||||
|
||||
require_cmd() {
|
||||
local cmd=$1
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: '$cmd' is required but not on PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
clone_or_update_opus() {
|
||||
if [[ ! -d "$OPUS_SRC_DIR/.git" ]]; then
|
||||
echo "Cloning opus ${OPUS_VERSION} into $OPUS_SRC_DIR"
|
||||
rm -rf "$OPUS_SRC_DIR"
|
||||
git clone --branch "$OPUS_VERSION" --depth 1 https://github.com/xiph/opus.git "$OPUS_SRC_DIR"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Reusing existing opus checkout at $OPUS_SRC_DIR"
|
||||
git -C "$OPUS_SRC_DIR" fetch --tags origin
|
||||
git -C "$OPUS_SRC_DIR" checkout "$OPUS_VERSION"
|
||||
}
|
||||
|
||||
build_one() {
|
||||
local abi=$1
|
||||
local build_dir="/tmp/opus-android-${abi}-build"
|
||||
local install_dir="/tmp/opus-android-${abi}"
|
||||
|
||||
echo "Building opus for ${abi} (API ${API_LEVEL})"
|
||||
rm -rf "$build_dir" "$install_dir"
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
# Opus 1.3.1's ARM NEON CMake path fails with recent NDK clang
|
||||
# (`celt_inner_prod_neon` implicit declaration). Keep this portable
|
||||
# baseline until we either patch Opus or upgrade the vendored version.
|
||||
cmake -S "$OPUS_SRC_DIR" -B "$build_dir" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$NDK_ROOT/build/cmake/android.toolchain.cmake" \
|
||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
|
||||
-DANDROID_ABI="$abi" \
|
||||
-DANDROID_PLATFORM="android-${API_LEVEL}" \
|
||||
-DCMAKE_INSTALL_PREFIX="$install_dir" \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DOPUS_BUILD_PROGRAMS=OFF \
|
||||
-DOPUS_BUILD_TESTING=OFF \
|
||||
-DOPUS_DISABLE_INTRINSICS=ON \
|
||||
-DOPUS_USE_NEON=OFF \
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
|
||||
cmake --build "$build_dir" --parallel
|
||||
cmake --install "$build_dir"
|
||||
|
||||
if [[ ! -f "$install_dir/lib/libopus.a" ]]; then
|
||||
echo "ERROR: expected $install_dir/lib/libopus.a" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
bar
|
||||
echo "Chanora Android opus build"
|
||||
echo "NDK : $NDK_ROOT"
|
||||
echo "Opus tag : $OPUS_VERSION"
|
||||
echo "API level : $API_LEVEL"
|
||||
bar
|
||||
|
||||
require_cmd git
|
||||
require_cmd cmake
|
||||
|
||||
if [[ ! -d "$NDK_ROOT" ]]; then
|
||||
echo "ERROR: Android NDK not found at $NDK_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
clone_or_update_opus
|
||||
|
||||
for abi_entry in "${ABIS[@]}"; do
|
||||
build_one "$abi_entry"
|
||||
done
|
||||
|
||||
bar
|
||||
echo "Android opus builds complete."
|
||||
for abi_entry in "${ABIS[@]}"; do
|
||||
echo " /tmp/opus-android-${abi_entry}/lib/libopus.a"
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user