Commit Graph

169 Commits

Author SHA1 Message Date
570feffc25 Keep contacts on Android
A contacts page lists friends, blocked and neutral clients with when and
where each was last seen, and opens each one's settings: custom and
phonetic nickname, type, how the name is shown, what to ignore, whether
it may whisper, a volume change, and its unique ID, copied on a tap.
Changes are stored at once. Another page holds the defaults new contacts
start from, and a TeamSpeak 3 settings.db can be imported.

A client's sheet on a server adds them as a friend or blocks them, or
edits and removes the contact they already are. Everything else runs on
core's contact store as on desktop, so a change reaches the server's
playback and tree straight away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:52:56 +00:00
5c0a5e4bcf Sort the Android settings into categories
Settings opens on a list of categories, for now Sound Settings (devices,
transmission, voice activation, processing, playback) and myTeamSpeak,
each a page of its own, with the icon pack beneath them. The nickname
field goes, as the connect screen already keeps it. The rows the pages
share move into their own file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:52:56 +00:00
e7741e8b34 Give the Material pack every icon TeamSpeak's packs have
The rest of default_colored_2014's keys get Material counterparts:
talk power, whisper, recording, text formatting, identities, add-ons,
permissions and the like. Ping levels are drawn as signal bars, since
Material's own tell their levels apart by a hairline, and the default
group icons as lettered badges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:43:07 +00:00
cdf81f3258 Share icon packs between desktop and Android
The Android app's Material icons become a TeamSpeak-format pack shipped in
core, which the desktop offers next to the TeamSpeak packs. Android now
draws its icons from packs too and imports TeamSpeak's own zips, falling
back to the Material pack for what they lack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:39:42 +00:00
a7aa122728 Sign in to myTeamSpeak and import what the account synchronises
The myTeamSpeak client protocol and its end-to-end encryption are
reverse-engineered (docs/myteamspeak): an HTTP POST of a framed protobuf,
a PBKDF2-HMAC-SHA512 login token and account key, an AES-GCM wrapped
item key and AES-CTR items with a SHA-512 trailer.

Options on desktop and Settings on Android gain a myTeamSpeak page: sign
in and out, and the account's bookmarks and identities, each picked for
import and marked when already here. The client stays signed in the way
the official one does, keeping the derived token and key rather than the
password; each read signs in afresh, and a sign-in the server no longer
takes signs out. Nothing is written to the account.

Items are the same Item_Data as the local TeamSpeak store, so they go
through the existing decoder and importer; a bookmark chosen alone brings
the identity it connects with, found by UUID or, as some name it, by
the identity's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:07:17 +00:00
73a15a8cfc Let AGC raise the noise floor further to bring up a distant voice
AGC2 won't amplify the background noise above -50 dBFS, which also caps
the gain for a voice barely above that noise: a distant talker ended up
5 dB quieter than a near one with noise suppression on, 12 dB with it
off. A "Boost quiet speech" setting (0-20 dB, default 0) raises the cap,
live, on desktop and Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:08:50 +00:00
491cbcb259 Leave the server when the app is swiped away
The process lived on without a screen, or was killed without a word, and
the server held our identity as a ghost until it timed out, refusing a
reconnect in the meantime as a clone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:31:50 +00:00
5fa7024bcb Follow the phone from one network to another
A server knows its clients by address, so moving between Wi-Fi and mobile
data leaves the connection with nothing to do but time out. The process's
sockets are now bound to the network a connection was made over: on a
change, the goodbye still leaves through the old network while it lasts,
and the connection is made again over the new one, back into the channel
we were in. A drop because the network vanished counts as a change too.

A server still holding the old connection refuses the new one as a clone
until it times out, so attempts are retried with a growing pause; a ban
ends it, and so do five minutes without getting back on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:31:41 +00:00
c8e99c8946 Show the microphone level for tuning voice activation
The settings show the level on TeamSpeak's scale, lit while it would be
sent, with the volume gate's threshold marked as the slider moves. While
connected it listens to the connection's own microphone, so it shows
exactly what goes out; otherwise it runs a test microphone of its own for
as long as the settings are on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:31:31 +00:00
62ced89701 Pick the Android audio devices, microphone preset and playback usage
Microphone and speaker are chosen separately, so a Bluetooth headset can
play in media quality while the phone's own microphone listens, instead of
the headset dropping into its call profile. Devices are kept by type and
address: Android hands out new ids whenever one reconnects, and a line that
lost its device comes back to it when it returns.

The microphone preset (voice recognition by default, or the platform's
call processing, unprocessed, ...) and whether voices play as media or as
a call are Android's own settings. Changing either reopens the open
streams in place. Communication mode, and the headset's call link that a
Bluetooth microphone needs, are held only while a stream needs them, and
are set up before that stream opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:31:22 +00:00
2281c125fa Keep the error id a server refused the last connect with
The message alone does not tell a ban from a refusal worth retrying.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
25ca586a50 Let a disconnect give up a connect still under way
Disconnecting only acted once connected, so a connect that was stuck or
retrying could not be called off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
ed526897dc Switch audio devices without reopening the connection
A new input device used to wait for the next connect, and the shared mixer
kept its line on the old output until it went idle for good. The microphone
now reopens on the new device as the settings are applied, and the mixer
moves its line over when the output device changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:30:23 +00:00
967fcadf77 Keep chat drafts per conversation and open each at its newest line
Switching conversations dropped the message being written, and showed
the next one at the scroll position of the last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:12:50 +00:00
70c6eb769c Say why a connection ended, and mark the channel we are in subscribed
- A failed connect keeps its reason in the status instead of a bare
  "Disconnected", as does a disconnect the server gave a reason for.
- The server never announces the subscription to the channel we start
  out in, although it keeps us subscribed after we leave it, so the
  channel we enter counts as subscribed.
- A client connecting or leaving the server is logged as that, not as
  switching from or to "channel #0": the server gives both the same
  reason, only with no channel on one side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:12:50 +00:00
f2c9143997 Pick up ts3j's refused-connect reason
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:12:50 +00:00
941365cab0 Give the Android app its real UI
- Connect screen with an address or a bookmark, which can be added and
  removed there.
- Server screen: the channel tree with each client's status, folding
  channels, joining (asking for a password where needed) and
  subscribing from a channel's sheet; private message, poke and volume
  from a client's sheet.
- Chat: the server log, the channel and private conversations with
  their history, unread marks, BBCode and clickable client, channel and
  web links. Chat lines are the HTML the desktop shows, turned into
  styled text.
- Voice bar: mute, deafen, away, and a hold-to-talk button in
  push-to-talk mode. The microphone permission is asked for on connect.
- Settings: nickname, transmission mode, voice activation, noise
  suppression, gain control and playback volume, in effect at once.

The session's state reaches Compose as immutable snapshots: the tree
at most every 30 ms, rebuilt only when channels or clients changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:21:51 +00:00
d83ba1145b Make a parsed spacer a value
So a frontend comparing snapshots of the tree sees an unchanged spacer
as unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:21:51 +00:00
35d6427456 Apply voice settings to a live microphone in one place
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:10:35 +00:00
d206b12a30 Move spacers, client status and chat entries into core
The Android client needs them too: which state a client's icon shows,
how spacer channels are named, and the HTML a chat line is written as.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:09:43 +00:00
c5a49fc7b3 Avoid JDK APIs newer than Android's minSdk 33 in core
Files.readString/writeString (API 36.1), Path.of, ByteBuffer.slice(int, int)
and Matcher.appendReplacement(StringBuilder) (API 34) threw NoSuchMethodError
on Android; connecting crashed in ChatLogStore. Each replacement keeps the
original desktop code in a comment above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 14:05:39 +00:00
5d67b0c1e3 Record what came of the codebase audit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:39 +00:00
51f19c2d03 Bring the README's architecture and features up to date
The architecture tree predated the session, chatlog, contacts, text,
file transfer and voice pipeline packages, and named a backend that no
longer exists. The feature list still described the first speech
detector and processing chain rather than the WebRTC ports, and knew
nothing of the jitter buffer; the limitations listed file transfers,
avatars and administration as missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
85c3ae6c8d Require Java 25, not 26
Nothing in the desktop client needs more than the FFM API, final since
Java 22. It builds, passes its tests and runs on OpenJDK 25, the current
LTS, which reaches more distributions. CI now tests on that minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
acb3efabd8 Pick up ts3j's DNS test that works offline
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
b6c5a772e6 Run the tests on every push
A Gitea Actions workflow that builds ts3j and the client and runs every
test; none needs a display or the native Opus library.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
71ac2c817c Test the channel tree, the event handler and settings
- the tree's sibling chain: arrival order, broken links, removal,
  reordering and reparenting, orphans, client order, path lookup
- the event handler, fed events parsed from the server's own wording:
  joins, moves, leaves, partial and clearing updates, server groups,
  and a channel's life from creation to deletion
- settings: saving and loading back, unknown values, and the one-time
  conversion of the old dBFS voice threshold

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
bf07f0bf49 Move what the servers share out of the main window
Which server captures with the microphone, handing it over, push-to-talk
and key presses reaching the right microphones, and away state and
nickname applied everywhere were all worked out in MainFrame. They move
to a Sessions class in core, next to ServerSession, where another
frontend can use them; the window keeps only the tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
f30cecf51c Keep channels the user collapsed collapsed
Every tree rebuild, which any join, leave or talk-power change causes,
expanded every row again, so a collapsed channel reopened within
seconds. The tree now remembers which channels were collapsed and
opens everything else; reopening one restores what is below it. While
the find bar filters, everything opens so every match shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
ae8d1f7083 Fail a download whose file server stops sending
The data socket had no read timeout, so a file server that went quiet
left the transfer, and its thread, waiting until the user cancelled.
Thirty seconds without data now fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
48a0032848 Parse every enum setting with the one generic parser
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:26 +00:00
3cdade9ad7 Keep the profile private and write it all at once
Settings, bookmarks (server passwords) and identities (private keys)
were written in place with the default permissions, readable by every
user on the machine, and a crash mid-write left a truncated file that
the next start silently replaced with defaults.

Every profile file is now written to a private temporary file, synced
and moved over the old one in one step. The profile folder itself is
made accessible to its owner only, which also covers files written
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
d7261c906d Let chat logs be turned off or kept elsewhere
Chat logs always went into the TeamSpeak 3 client's own folder. A new
Chat page in Options can turn them off, or point them at another
folder; left empty, they are still shared with the TeamSpeak 3 client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
1a3a3e37c1 Write both kinds of chat message through one method
appendMessage and appendPrivateMessage built the same entry twice; they
now differ only in which log they pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
a5c4ba1eb5 Load chat history from the end of the log only
Chat logs grow forever, and every connect and every private chat opened
read the whole log to keep its last 1000 entries. History now reads the
last 4 MiB, which holds far more than that; searching still reads the
whole log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
f9b2f4d7cb Keep every line of a multi-line message in chat history
The text log has a message's line breaks in it, but history was read one
line at a time, so every line after the first failed to parse and was
dropped. A line that starts no entry of its own now continues the
message before it, which also recovers what earlier versions wrote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
8e9c461424 Wait for the capture loop when the microphone stops
stop() closed the encoder and cleared the transmit state while the
capture loop could still be in its last frame, which could turn the
transmit indicator back on, or overlap the loop of a quick restart on
the same speech detector and processing chain. It now closes the line
to wake the loop and waits for it to finish before tearing down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
12ca585d90 Open a new line when a speaker's playback line dies
Both desktop backends dropped writes to a line whose device had gone
away, and the per-speaker playout ignored any failure, so that speaker
stayed silent for the rest of the session. A write to a dead line now
throws; the playout drops that line and the speaker's next packet
opens a fresh one. Sound effects and the microphone test's loopback
reopen their line the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
6234ec6d97 Give back a speaker's line after 30 s of silence
Every client that ever spoke kept an open playback line and a thread
until it left the server. A speaker silent for 30 s now has both
released, and its next talk burst opens them again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
0083dc8ad6 Play incoming voice through TeamSpeak's adaptive jitter buffer
Each speaker's stream held a fixed 60 ms and gave up after 200 ms of
concealment, so a jittery connection concealed constantly and a stall
ended the burst. It now runs through a port of the libspeex jitter
buffer the official client links, configured and driven as it does:

- the delay adapts to the measured jitter, aiming for at most 1% of
  packets arriving late
- a speaker keeps one timeline, never re-anchored between talk bursts,
  so what the buffer learned carries over; only 5 s without packets
  drops it
- the talker's stop marker never enters the buffer; playback ends when
  it reaches it, and its slot is played as silence, not concealed

A pull now always hands out one 20 ms frame: what a longer packet or a
delay increase produces beyond that is played on the following pulls,
which Android's mixer used to cut off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
7cb4f47a81 Show an avatar, description or away message being cleared
Client and channel updates were only applied when the field had a
value, since ts3j reads a missing field back as an empty string. A
removed avatar, an emptied description or a cleared away message
arrives as the field without a value, so they stayed as they were. Such
fields are now applied whenever the update carries them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
8c735a05b8 Look permissions up by name directly
selfPermissionValue scanned every permission the server lists, several
times per context menu. The model now keeps a name index next to the
id one. Also drops primaryServerGroupName, which nothing calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
67f8f46cab Publish the channel tree's lists whole instead of refilling them
buildTree() cleared and refilled the children and client lists inside
the shared channel nodes, so a UI still walking the previous tree could
see them change under it. It now builds new lists and publishes them
unmodifiable. The node fields, written by the event and audio threads
and read by the UI, are volatile.

Whether a channel is full is now counted from the model, rather than
from a tree that may not have been rebuilt since the last join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
665316d515 Hand the microphone between tabs in the order it was asked
Each hand-over ran on a fresh thread, so two quick tab switches could
start the new tab's capture before the old one stopped, or stop it after
it started. They now run one after another on a single thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
5b1809cc2d Hand out hotkey bindings as a snapshot
Hotkeys.all() returned its synchronised list, and every reader had to
lock it while iterating: a lock owned by one class and taken by four
others. It now returns a copy, and the lock stays inside Hotkeys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 11:25:25 +00:00
eeccb495eb Name chat logs by the server's unique id for guests too
The id came from initserver, which never carries it, then from
serverinfo and servergetvariables, which a guest may not use; chat logs
only worked with admin rights. ts3j now derives it from the server's
handshake key, as the official client does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:35:41 +00:00
d074ffaa7a Tell permission errors apart by the server's error id
The sound for a refused action was picked by looking for "permission"
or "insufficient" in the message, which also matched errors such as an
invalid permission id. The server's error ids for insufficient client,
group and permission power say it exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:31:28 +00:00
d28bb57598 Release a previous connection's resources on connect
A connect on an instance that was still connected, or still
connecting, replaced the socket and audio pipelines without closing
them. The fields shared between the connect, event, action and UI
threads are now volatile, and read once where they are used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:30:34 +00:00
f350b19d31 Run a connection's requests on its own pool
Each action started a thread that read the socket field once it ran; a
disconnect in between cleared it, and the user saw "Could not join
channel: NullPointerException". Actions now capture the socket when
they are issued and run on a pool that lives as long as the connection.
What fails because the connection went away is no longer reported as an
error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:29:23 +00:00
ba5de5a8a8 Give the importer test a fresh profile every run
The test home under target/ outlives a run, so the identity the first
run imported made every later run fail. The test now points AppDirs at
a temporary directory of its own, which also works when another test
resolved the profile first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 10:27:11 +00:00