6.9 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
TeamSpeak Android native client with a two-layer architecture:
- Protocol layer: Go + teamspeak-go → compiled to
.aarvia gomobile - UI layer: Kotlin + Jetpack Compose + Material Design 3
Build Commands
Full Build (Recommended)
# Windows
build.bat
# Linux/macOS
./build.sh
Manual Build (Two Steps Required)
Step 1: Compile Go → AAR
cd go
set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 # Windows only
gomobile bind -target=android -androidapi=26 -ldflags="-linkmode=external -extldflags=-Wl,--hash-style=both" -o ../android/app/libs/teamspeak.aar ./teamspeak
Step 2: Build Android APK
cd android
gradlew.bat assembleDebug # Windows
./gradlew assembleDebug # Linux/macOS
Development Workflow
- Modified Go code (
go/): Re-run Step 1, then run from IDE - Modified Kotlin code (
android/): Just run from IDE (IntelliJ/Android Studio)
Open the android/ directory (not root) in IntelliJ IDEA or Android Studio.
Architecture
Go ↔ Kotlin Bridge
Communication flows through two bridge layers:
- Go side (
go/teamspeak/bridge.go): ExportsTSClientclass via gomobile - Kotlin side (
android/app/src/main/java/com/tsmobile/app/TSBridge.kt): Wraps gomobile API
Key constraints (gomobile limitations):
- Cannot export
[]string,[]*T, or Goerrortypes - Complex data passed as JSON strings (channels, clients)
- Events delivered via callback interfaces
- All JNI callbacks serialized through an event queue to avoid threading issues
Event flow: Go library → event queue → single consumer goroutine → JNI callback → Kotlin callback → ViewModel
Android MVVM + Repository Pattern
Go Library → TSBridge (JNI) → ServerViewModel (callbacks) → Repository (state) → ViewModels → Compose Screens
User Actions → Compose UI → ViewModel methods → TSBridge → Go Library
Repository (data/Repository.kt): Singleton object, single source of truth for shared state — channels, clients, channel→clients mapping, current channel, unread counts. Uses ConcurrentHashMap for thread-safe message archives (max 500 per session, keyed by targetMode_targetId).
ViewModels: Cross-references set by NavGraph (e.g. serverViewModel.channelViewModel = channelViewModel). ServerViewModel dispatches all bridge callbacks to other ViewModels.
State machines (sealed classes in data/Models.kt): ConnectionState, MessageSendState, MessageDeliveryState, VoiceState, SyncState, ChannelSwitchState — use when for pattern matching.
Voice System Ownership Split
- Go side owns the full decode/mix pipeline: per-client jitter buffers, Opus decoders, stereo mixing. Delivers mixed PCM16 as 20ms frames (48kHz, interleaved stereo) via
onPCMcallback. Speaking detection: 400ms silence timeout. - Kotlin side owns capture:
AudioRecord(mono, 48kHz) →OpusEncoder→TSBridge.sendVoice(). Noise suppressor support. Speaking detection via RMS threshold (0.015).
Message Delivery Confirmation
Messages appear immediately as PENDING in UI. Server echo (OnTextMessage with matching content) confirms → SENT. 10-second timeout → FAILED. Duplicate detection prevents self-message re-archival.
Navigation Routes
Four routes in NavGraph.kt: server_config (connection form) → channel_list (channel tree + members) → chat (text chat) → kicked (kick notification). Navigation driven by ConnectionState changes via LaunchedEffect.
Known SDK Limitations
- No channel CRUD events: The SDK does not fire events for channel create/update/delete. Channel list is refreshed after 5 minutes of staleness or before channel switch operations.
- Unreliable ChannelID in enter events:
notifycliententerviewChannelID is not effective per SDK docs. Client enter/leave events trigger a fullclientlistrefresh instead of incremental updates. - Auto-reconnect disabled by default: Frequent reconnect attempts cause server-side rate limiting/bans.
Thread Safety
TSClient.mumutex protects Go-side client accessTSBridge.connectLocksynchronizes Kotlin-side connection lifecycleconnectionGenerationcounter prevents stale callbacks from reaching current session- Event queue PCM events capped at 12 with oldest eviction to prevent buildup
Critical Build Details
Required Flags for gomobile
The -ldflags are mandatory to avoid runtime crashes:
-
-linkmode=external: Use NDK's external linker instead of Go's built-in linker- Why: Prevents
SIGSEGVcrashes due to signal handling conflicts between Go runtime and Android - Go uses SIGSEGV for GC and goroutine scheduling; Android's memory protection blocks this
- Why: Prevents
-
-extldflags=-Wl,--hash-style=both: Generate compatible ELF hash tables- Why: Go 1.24+ uses
DT_SUNW_HASHby default; Android requiresDT_HASHorDT_GNU_HASH - Without this:
dlopen failed: empty/missing DT_HASH/DT_GNU_HASHerror
- Why: Go 1.24+ uses
Local Patches
The go/_patches/github.com/honeybbq/teamspeak-go/ directory contains modified upstream code:
- Fixes 32-bit integer overflow issues (
math.MaxUint32→ platform-specific limits) - Applied via
go.modreplace directive:replace github.com/honeybbq/teamspeak-go => ./_patches/github.com/honeybbq/teamspeak-go
Do not remove this directory or the replace directive.
Go Module and CGo
Go module name is tsmobile. Unlike upstream teamspeak-go (zero CGO), this project enables CGO for the Android libopus decoder. Pre-built static libraries live in go/teamspeak/.opus/lib/{abi}/libopus.a for all four Android ABIs.
Common Issues
"javac: 非法字符" or "GBK unmappable character"
Set encoding before running gomobile on Windows:
set JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8
App crashes with "SIGSEGV" on startup
Missing -linkmode=external flag. Rebuild AAR with correct flags.
App crashes with "empty/missing DT_HASH"
Missing -Wl,--hash-style=both flag. Rebuild AAR with correct flags.
"Unresolved reference: teamspeak"
AAR not compiled or not in android/app/libs/teamspeak.aar. Run Step 1 of manual build.
Error Conventions
- Go bridge: Empty string return = success, non-empty = error message. All IDs are strings for JSON transport.
- Kotlin: Chinese-language user-facing errors.
ServerViewModel.classifyError()maps raw errors to friendly messages.ChannelViewModel.mapMoveError()handles channel switch errors.
Testing
Run from IDE with connected device or emulator. Check Logcat in IDE for runtime logs.
Go unit tests exist for the audio receive pipeline (go/teamspeak/receive_audio_test.go): cd go && go test ./teamspeak/