Manage identities on Android, pick one per bookmark, and give identities their own nicknames

Settings -> Identities lists them and creates, imports (an identity file or a
TeamSpeak 3 settings.db), exports, renames and removes them, picks the
default and improves an identity's security level; the search runs on until
reached or stopped, whichever page is on screen. The Connect screen and each
bookmark choose the identity to connect with.

An identity keeps a nickname and phonetic nickname, in TeamSpeak's own INI
keys, on both clients. A connect goes by the bookmark's nickname, else the
identity's, else the global one; the phonetic one is sent in clientinit.
Imported bookmarks no longer copy their identity's nickname.

IdentityStore is now safe to share with the thread a connect resolves its
identity on, and imports and exports through streams for Android's document
picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 00:17:14 +00:00
parent 6aa4be38ee
commit 1dda20d1b2
16 changed files with 878 additions and 96 deletions

View File

@@ -31,6 +31,8 @@ import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -53,6 +55,10 @@ public final class IdentitiesDialog extends JDialog {
private final JTextField nameField = new JTextField();
private final JTextField uidField = new JTextField();
private final JTextField levelField = new JTextField();
private final JTextField nicknameField = new JTextField();
private final JTextField phoneticField = new JTextField();
/** The identity the nickname fields belong to, which the list may already have moved off. */
private IdentityEntry shown;
public IdentitiesDialog(Frame owner, IdentityStore identities, Settings settings,
Bookmarks bookmarks, Runnable onChanged) {
@@ -124,6 +130,19 @@ public final class IdentitiesDialog extends JDialog {
addRow(form, c, row++, "Name:", nameField);
addRow(form, c, row++, "Unique ID:", uidField);
addRow(form, c, row++, "Security level:", levelField);
addRow(form, c, row++, "Nickname:", nicknameField);
addRow(form, c, row++, "Phonetic nickname:", phoneticField);
nicknameField.setToolTipText("Empty: the global nickname");
for (JTextField field : new JTextField[]{nicknameField, phoneticField}) {
field.addActionListener(e -> applyNicknames());
field.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
applyNicknames();
}
});
}
c.gridx = 0;
c.gridy = row;
@@ -172,13 +191,39 @@ public final class IdentitiesDialog extends JDialog {
}
private void showDetails(IdentityEntry e) {
applyNicknames();
shown = e;
nicknameField.setEnabled(e != null);
phoneticField.setEnabled(e != null);
nicknameField.setText(e == null ? "" : e.getNickname());
phoneticField.setText(e == null ? "" : e.getPhoneticNickname());
nameField.setText(e == null ? "" : e.getName());
uidField.setText(e == null ? "" : e.getUniqueId());
levelField.setText(e == null ? "" : Integer.toString(e.getSecurityLevel()));
}
@Override
public void dispose() {
// Escape closes without the field losing focus first.
applyNicknames();
super.dispose();
}
// ---- actions ----
private void applyNicknames() {
IdentityEntry e = shown;
if (e == null || identities.byId(e.getId()) == null) return;
String nickname = nicknameField.getText().trim();
String phonetic = phoneticField.getText().trim();
if (nickname.equals(e.getNickname()) && phonetic.equals(e.getPhoneticNickname())) return;
try {
identities.setNicknames(e, nickname, phonetic);
} catch (Exception ex) {
error("Could not save the nicknames", ex);
}
}
private void newIdentity() {
String name = JOptionPane.showInputDialog(this, "Name for the new identity:", "New identity");
if (name == null || name.trim().isEmpty()) return;

View File

@@ -640,7 +640,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
private void startConnection(String address, int port, String nickname, String password, String identityId,
String channel, String channelPassword, boolean replaceCurrent) {
settings.lastAddress = address + ":" + port;
settings.nickname = nickname;
if (!nickname.isBlank()) settings.nickname = nickname;
settings.serverPassword = password;
settings.save();
@@ -653,8 +653,7 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
/** Connects on the current tab, leaving whatever server it is on. */
private void connectToBookmark(Bookmark b) {
String nick = (b.nickname != null && !b.nickname.isBlank()) ? b.nickname : settings.nickname;
startConnection(b.address, b.port, nick, b.password, b.identityId, b.channel, b.channelPassword, true);
startConnection(b.address, b.port, b.nickname == null ? "" : b.nickname, b.password, b.identityId, b.channel, b.channelPassword, true);
}
private void addCurrentServerBookmark() {