# 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](https://github.com/honeybbq/teamspeak-go) → compiled to `.aar` via gomobile
- **UI layer**: Kotlin + Jetpack Compose + Material Design 3

## Build Commands

### Full Build (Recommended)
```bash
# Windows
build.bat

# Linux/macOS
./build.sh
```

### Manual Build (Two Steps Required)

**Step 1: Compile Go → AAR**
```bash
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**
```bash
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) → `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**: `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:
```bash
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/`
