feat: integrate chat voice and diagnostics client
This commit is contained in:
@@ -69,8 +69,8 @@ android {
|
||||
// Per-ABI delivery is handled by the AAB bundle splits below
|
||||
// (SDD-109 item 2) rather than a fat APK.
|
||||
ndk {
|
||||
// DEC-032 RESOLVED (2026-05-18): canonical three-ABI set
|
||||
// restored after the audiopus_sys + cmake-rs + NDK toolchain-file
|
||||
// DEC-032 RESOLVED (2026-05-18): canonical two-ABI set
|
||||
// (arm64-v8a + x86_64) restored after the audiopus_sys
|
||||
// ANDROID_ABI propagation gap was closed by the workspace
|
||||
// [patch.crates-io] override pinning `cmake` to the fork
|
||||
// carrying cmake-rs PR #257 (forwards ANDROID_ABI /
|
||||
@@ -138,9 +138,10 @@ android {
|
||||
}
|
||||
|
||||
// SDD-109 item 2: Android App Bundle (.aab) split configuration.
|
||||
// Combined with the SDD-073 abiFilters set, this yields three
|
||||
// native splits (arm64-v8a, armeabi-v7a, x86_64), per-language
|
||||
// resource delivery, and per-density resource delivery.
|
||||
// Combined with the SDD-073 abiFilters set, this yields two
|
||||
// native splits (arm64-v8a, x86_64), per-language resource
|
||||
// delivery, and per-density resource delivery. Each device
|
||||
// receives only its matching ABI .so — no cross-ABI bundling.
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = true
|
||||
@@ -221,7 +222,6 @@ val chanoraAndroidMinSdk: Provider<String> =
|
||||
// for the per-ABI copy and for the inputs/outputs declaration.
|
||||
val abiToRustTriple: Map<String, String> = mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"armeabi-v7a" to "armv7-linux-androideabi",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
)
|
||||
|
||||
@@ -278,7 +278,7 @@ val checkRustBridgeToolchain = tasks.register("checkRustBridgeToolchain") {
|
||||
if (missing.isNotEmpty()) {
|
||||
throw GradleException(
|
||||
"[SDD-118] Missing Android Rust target(s): ${missing.joinToString(", ")}. " +
|
||||
"Install: rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android"
|
||||
"Install: rustup target add aarch64-linux-android x86_64-linux-android"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -312,6 +312,15 @@ fun registerCargoNdkBuildTask(profile: String): TaskProvider<*> {
|
||||
.takeUnless { it.isNullOrBlank() }
|
||||
?: android.ndkDirectory.absolutePath
|
||||
val cmakeToolchainFile = "$effectiveNdkPath/build/cmake/android.toolchain.cmake"
|
||||
val homeDir = System.getenv("HOME") ?: System.getProperty("user.home")
|
||||
val rustupBinDir = listOf(
|
||||
"$homeDir/.cargo/bin",
|
||||
"/opt/homebrew/opt/rustup/bin",
|
||||
"/usr/local/opt/rustup/bin"
|
||||
).firstOrNull { File(it).resolve("cargo").exists() }
|
||||
val rustupHome = System.getenv("RUSTUP_HOME") ?: "$homeDir/.rustup"
|
||||
val cargoHome = System.getenv("CARGO_HOME") ?: "$homeDir/.cargo"
|
||||
val rustupToolchain = System.getenv("RUSTUP_TOOLCHAIN") ?: "stable-aarch64-apple-darwin"
|
||||
|
||||
// SDD-118 item 13 (corrected): per-ABI Exec sub-tasks. Each runs an
|
||||
// isolated `cargo ndk -t <abi> ... -- build ...` so ANDROID_ABI is set
|
||||
@@ -361,15 +370,18 @@ fun registerCargoNdkBuildTask(profile: String): TaskProvider<*> {
|
||||
// Any CARGO_TARGET_* env vars set by an outer build wrapper
|
||||
val cleanEnv = buildMap<String, String> {
|
||||
// Pass-through from the calling environment, only the safe set.
|
||||
System.getenv("PATH")?.let { put("PATH", it) }
|
||||
System.getenv("HOME")?.let { put("HOME", it) }
|
||||
System.getenv("PATH")?.let { path ->
|
||||
put("PATH", listOfNotNull(rustupBinDir, path).joinToString(":"))
|
||||
}
|
||||
put("HOME", homeDir)
|
||||
System.getenv("USER")?.let { put("USER", it) }
|
||||
System.getenv("TMPDIR")?.let { put("TMPDIR", it) }
|
||||
System.getenv("TEMP")?.let { put("TEMP", it) }
|
||||
System.getenv("LANG")?.let { put("LANG", it) }
|
||||
System.getenv("LC_ALL")?.let { put("LC_ALL", it) }
|
||||
System.getenv("CARGO_HOME")?.let { put("CARGO_HOME", it) }
|
||||
System.getenv("RUSTUP_HOME")?.let { put("RUSTUP_HOME", it) }
|
||||
put("CARGO_HOME", cargoHome)
|
||||
put("RUSTUP_HOME", rustupHome)
|
||||
put("RUSTUP_TOOLCHAIN", rustupToolchain)
|
||||
System.getenv("JAVA_HOME")?.let { put("JAVA_HOME", it) }
|
||||
// Per-task explicit settings (override any pass-through).
|
||||
// SDD-118 item 5: audiopus_sys needs libopus built from source
|
||||
@@ -444,7 +456,6 @@ val buildRustBridgeRelease = registerCargoNdkBuildTask("release")
|
||||
// $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/.
|
||||
val abiToNdkSysrootTriple: Map<String, String> = mapOf(
|
||||
"arm64-v8a" to "aarch64-linux-android",
|
||||
"armeabi-v7a" to "arm-linux-androideabi",
|
||||
"x86_64" to "x86_64-linux-android",
|
||||
)
|
||||
fun registerJniLibsCopyTask(profile: String): TaskProvider<Copy> {
|
||||
|
||||
+10
-13
@@ -11,8 +11,8 @@
|
||||
# the app module — these are looked up by signature from C/Rust.
|
||||
# 3. The Android voice foreground service (SDD-107) — referenced from
|
||||
# the manifest by FQN and from JNI via static start/stop helpers.
|
||||
# 4. The Android audio mode controller (SDD-108) — referenced from
|
||||
# JNI via GlobalRef.
|
||||
# 4. The Android audio mode controller (SDD-108) — audio mode is
|
||||
# managed in Rust via direct JNI (engine.rs), not a Kotlin class.
|
||||
# 5. Kotlin metadata required by FRB-generated bindings (SDD-079).
|
||||
|
||||
# --- 1. chanora_bridge / flutter_rust_bridge JNI surface (SDD-105) -----------
|
||||
@@ -46,21 +46,18 @@
|
||||
}
|
||||
|
||||
# --- 3. AndroidVoiceForegroundService (SDD-107) ------------------------------
|
||||
# The service is referenced by FQN from AndroidManifest.xml and by JNI from
|
||||
# the Rust audio engine (start/stop helpers per SDD-107 item 10). Keep the
|
||||
# class and its public/static members.
|
||||
-keep class app.chanora.chanora_flutter.AndroidVoiceForegroundService { *; }
|
||||
# Covered by the JNI-surface keep block above.
|
||||
|
||||
# --- 4. AndroidAudioModeController (SDD-108) ---------------------------------
|
||||
# Referenced from Rust via GlobalRef + static method invocation.
|
||||
# Wave 2B-2 owns the class; keep rule is forward-looking but harmless if
|
||||
# the class does not yet exist (R8 ignores keep rules for missing classes).
|
||||
-keep class app.chanora.chanora_flutter.AndroidAudioModeController { *; }
|
||||
# --- 4. Android Audio Mode Controller (SDD-108) -------------------------
|
||||
# SDD-108 in-call audio mode (AudioManager.setMode) is implemented in the
|
||||
# Rust audio engine (crates/chanora_audio/src/engine.rs) via direct JNI,
|
||||
# not through a Kotlin controller class. No keep rule is needed; the
|
||||
# engine.rs android_set_audio_mode / android_get_audio_mode helpers
|
||||
# resolve AudioManager at runtime via ndk_context.
|
||||
|
||||
# --- 5. Kotlin metadata + serialisation (SDD-079) ----------------------------
|
||||
-keepattributes *Annotation*, Signature, InnerClasses, EnclosingMethod
|
||||
-keep class kotlin.Metadata { *; }
|
||||
|
||||
# Defer to Flutter's own keep rules for the embedding layer; the Flutter
|
||||
# Gradle plugin contributes those automatically. If a future SDD revision
|
||||
# disables R8 entirely, this file can be reduced to a single TODO comment.
|
||||
# Gradle plugin contributes those automatically.
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
protocol layer to dial a TeamSpeak-compatible server over UDP. -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- SRS-100: Network diagnostics. Allows the core to query active
|
||||
network type and state for the diagnostic export. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<!-- SDD-115: Prevent CPU sleep during active voice sessions.
|
||||
Keeps the audio pipeline running when the screen locks. -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- SRS-025: Haptic feedback on PTT key press. Enables
|
||||
VibrationEffect for transmit-mode tactile confirmation. -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- SDD-trace: SDD-106 AndroidPermissionRequester.
|
||||
Required for the audio engine (chanora_audio) to open the
|
||||
capture stream for voice transmission. The runtime grant
|
||||
@@ -56,7 +68,8 @@
|
||||
<application
|
||||
android:label="Chanora"
|
||||
android:name="app.chanora.chanora_flutter.ChanoraApplication"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
+4
-6
@@ -324,13 +324,11 @@ class AndroidPermissionRequester {
|
||||
callback: ((PermissionState) -> Unit)?,
|
||||
) {
|
||||
callback?.invoke(state)
|
||||
// The stateChangeListener is wired by MainActivity to invoke
|
||||
// METHOD_PERMISSION_STATE_CHANGED on ANDROID_PERMISSIONS, which
|
||||
// publishes BridgeEvent::PermissionState to the bridge event
|
||||
// stream. See MainActivity.configureFlutterEngine (SDD-106 §5).
|
||||
stateChangeListener?.invoke(permission, state)
|
||||
// TODO(Wave 2B follow-up): wire this into a Flutter MethodChannel
|
||||
// named MethodChannels.ANDROID_PERMISSIONS, invoking method
|
||||
// MethodChannels.METHOD_PERMISSION_STATE_CHANGED. The Dart
|
||||
// handler then publishes the corresponding
|
||||
// BridgeEvent::PermissionState onto the bridge event stream
|
||||
// (SDD-106 §5).
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+8
-28
@@ -71,24 +71,9 @@ class AndroidVoiceForegroundService : Service() {
|
||||
*/
|
||||
private const val CHANNEL_ID = "chanora.voice.session"
|
||||
|
||||
/**
|
||||
* Notification channel user-visible name.
|
||||
*
|
||||
* TODO(localization): SDD-107 §4 requires this to be sourced
|
||||
* from a product string resource. Hard-coded here for the
|
||||
* Wave 2B implementation slice; localised strings land
|
||||
* alongside the broader Android string-resource pass.
|
||||
*/
|
||||
private const val CHANNEL_NAME = "Voice session"
|
||||
|
||||
/**
|
||||
* Notification channel description (product copy only, no
|
||||
* server-supplied content per SDD-107 §5 privacy note).
|
||||
*
|
||||
* TODO(localization): see [CHANNEL_NAME].
|
||||
*/
|
||||
private const val CHANNEL_DESCRIPTION =
|
||||
"Shown while a Chanora voice session is active."
|
||||
// SDD-107 §4: notification strings are sourced from
|
||||
// res/values/strings.xml and accessed via the service context
|
||||
// in instance methods below (ensureChannel, buildNotification).
|
||||
|
||||
/**
|
||||
* Stable notification id ("CHAN") per SDD-107 §5. Must remain
|
||||
@@ -298,10 +283,10 @@ class AndroidVoiceForegroundService : Service() {
|
||||
if (existing != null) return
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
getString(R.string.voice_session_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
).apply {
|
||||
description = CHANNEL_DESCRIPTION
|
||||
description = getString(R.string.voice_session_channel_description)
|
||||
setShowBadge(false)
|
||||
}
|
||||
nm.createNotificationChannel(channel)
|
||||
@@ -329,14 +314,9 @@ class AndroidVoiceForegroundService : Service() {
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_speakerphone)
|
||||
// TODO(SDD-107 §5): replace stat_sys_speakerphone with the
|
||||
// product icon `ic_chanora_voice` once the drawable lands
|
||||
// in res/drawable. Using a platform-provided icon as the
|
||||
// interim placeholder keeps the build green without
|
||||
// touching res/* (out of this slice's file set).
|
||||
.setContentTitle("Chanora — Voice session active")
|
||||
.setContentText("Microphone may be in use.")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(getString(R.string.voice_session_notification_title))
|
||||
.setContentText(getString(R.string.voice_session_notification_text))
|
||||
.setOngoing(true)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- SDD-107 §4: Chinese (Simplified) localized notification strings.
|
||||
Product-owned copy; no server-supplied content per SDD-107 §5. -->
|
||||
<string name="voice_session_channel_name">语音会话</string>
|
||||
<string name="voice_session_channel_description">Chanora 语音会话处于活动状态时显示。</string>
|
||||
<string name="voice_session_notification_title">Chanora — 语音会话进行中</string>
|
||||
<string name="voice_session_notification_text">麦克风可能正在使用中。</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- SDD-107 §4: Voice session foreground service notification strings.
|
||||
These are product-owned strings; server-supplied content is never
|
||||
placed in notification copy per SDD-107 §5 privacy note. -->
|
||||
<string name="voice_session_channel_name">Voice session</string>
|
||||
<string name="voice_session_channel_description">Shown while a Chanora voice session is active.</string>
|
||||
<string name="voice_session_notification_title">Chanora — Voice session active</string>
|
||||
<string name="voice_session_notification_text">Microphone may be in use.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user