Files
ts-mobile-go/CLAUDE.md
T
2026-07-23 19:37:20 +08:00

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 .aar via gomobile
  • UI layer: Kotlin + Jetpack Compose + Material Design 3

Build Commands

# 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:

  1. Go side (go/teamspeak/bridge.go): Exports TSClient class via gomobile
  2. Kotlin side (android/app/src/main/java/com/tsmobile/app/TSBridge.kt): Wraps gomobile API

Key constraints (gomobile limitations):

  • Cannot export []string, []*T, or Go error types
  • 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 onPCM callback. Speaking detection: 400ms silence timeout.
  • Kotlin side owns capture: AudioRecord (mono, 48kHz) → OpusEncoderTSBridge.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: notifycliententerview ChannelID is not effective per SDK docs. Client enter/leave events trigger a full clientlist refresh instead of incremental updates.
  • Auto-reconnect disabled by default: Frequent reconnect attempts cause server-side rate limiting/bans.

Thread Safety

  • TSClient.mu mutex protects Go-side client access
  • TSBridge.connectLock synchronizes Kotlin-side connection lifecycle
  • connectionGeneration counter 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:

  1. -linkmode=external: Use NDK's external linker instead of Go's built-in linker

    • Why: Prevents SIGSEGV crashes due to signal handling conflicts between Go runtime and Android
    • Go uses SIGSEGV for GC and goroutine scheduling; Android's memory protection blocks this
  2. -extldflags=-Wl,--hash-style=both: Generate compatible ELF hash tables

    • Why: Go 1.24+ uses DT_SUNW_HASH by default; Android requires DT_HASH or DT_GNU_HASH
    • Without this: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH error

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.mod replace 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/