From 7ef1162f976cbafabbc869c9e42c23471d5b0b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Ne=C4=8Das?= Date: Mon, 16 Nov 2015 10:33:20 +0000 Subject: [PATCH 001/257] Translated using Weblate (Czech) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_cs.properties | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/resources/i18n/strings_cs.properties b/src/main/resources/i18n/strings_cs.properties index f77942e2..72902c49 100644 --- a/src/main/resources/i18n/strings_cs.properties +++ b/src/main/resources/i18n/strings_cs.properties @@ -239,3 +239,24 @@ s_XITD=Nedostupný s_NCU0=Chyba kontaktu s_NTTQ=Chyba: s_L4ZD=Server nenalezen +s_KPWG=Prázdný +s_ZX32=Smazaný +s_LBBT=Vy +s_ZUZ9=Stahování souboru selhalo +s_JRCQ=Nahrávání souboru selhalo +s_JSD2=Neobvyklá chyba: +s_BLFT=Automaticky opustíte tuto skupinu. +s_74SC=Vyberte účastníky: +s_B5NX=Upravit skupinový chat +s_U5RT=vytvořil tuto skupinu +s_5X07=opustil tuto skupinu +s_FWZZ=upravil tuto skupinu +s_OOS3=Připojování… +s_0WD8=Odpojování… +s_Z1GT=Nepodařilo se načíst všechny soubory s klíči z archivu. +s_MMBD=Hledání… +s_ZZWW=Odesílat aktivitu při chatu (píše…) ostatním uživatelům +s_PC1G=píše… +s_KRQ6=Zadejte heslo… +s_2R2P=načítání… +s_307W=stahování… From f7ca96b3af3b4e3018880f5b077492b6495b84fa Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 16 Nov 2015 16:34:34 +0100 Subject: [PATCH 002/257] client: moved code for sending messages to new class --- src/main/java/org/kontalk/client/Client.java | 139 ++------------ .../org/kontalk/client/KonMessageSender.java | 169 ++++++++++++++++++ 2 files changed, 180 insertions(+), 128 deletions(-) create mode 100644 src/main/java/org/kontalk/client/KonMessageSender.java diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 3fd1212a..d862cc10 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -44,25 +44,13 @@ import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; -import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.system.Config; import org.kontalk.misc.KonException; -import org.kontalk.crypto.Coder; import org.kontalk.crypto.PersonalKey; -import org.kontalk.model.Chat; import org.kontalk.misc.JID; -import org.kontalk.model.GroupChat; -import org.kontalk.model.GroupChat.GID; -import org.kontalk.model.KonMessage.Status; import org.kontalk.model.OutMessage; -import org.kontalk.model.MessageContent; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.Preview; -import org.kontalk.model.Transmission; import org.kontalk.system.Control; import org.kontalk.system.RosterHandler; -import org.kontalk.util.ClientUtils; -import org.kontalk.util.EncodingUtils; /** * Network client for an XMPP Kontalk Server. @@ -80,12 +68,17 @@ public final class Client implements StanzaListener, Runnable { private static enum Command {CONNECT, DISCONNECT}; private final Control mControl; + + private final KonMessageSender mMessageSender; + private KonConnection mConn = null; public Client(Control control) { mControl = control; //mLimited = limited; + mMessageSender = new KonMessageSender(this, mControl); + // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; } @@ -208,6 +201,10 @@ public void disconnect() { mControl.setStatus(Control.Status.DISCONNECTED); } + public boolean isConnected() { + return mConn != null && mConn.isAuthenticated(); + } + /** * The full JID of the user currently logged in. */ @@ -219,117 +216,7 @@ public Optional getOwnJID() { } public boolean sendMessage(OutMessage message, boolean sendChatState) { - // check for correct receipt status and reset it - Status status = message.getStatus(); - assert status == Status.PENDING || status == Status.ERROR; - message.setStatus(Status.PENDING); - - if (!this.isConnected()) { - LOGGER.info("not sending message(s), not connected"); - return false; - } - - MessageContent content = message.getContent(); - Optional optAtt = content.getAttachment(); - if (optAtt.isPresent() && !optAtt.get().hasURL()) { - LOGGER.warning("attachment not uploaded"); - message.setStatus(Status.ERROR); - return false; - } - - boolean encrypted = - message.getCoderStatus().getEncryption() != Coder.Encryption.NOT || - message.getCoderStatus().getSigning() != Coder.Signing.NOT; - - Chat chat = message.getChat(); - - Message protoMessage = encrypted ? new Message() : rawMessage(content, chat, false); - - protoMessage.setType(Message.Type.chat); - protoMessage.setStanzaId(message.getXMPPID()); - String threadID = chat.getXMPPID(); - if (!threadID.isEmpty()) - protoMessage.setThread(threadID); - - // extensions - - // TODO with group chat? (for muc "NOT RECOMMENDED") - if (!chat.isGroupChat()) - protoMessage.addExtension(new DeliveryReceiptRequest()); - - if (sendChatState) - protoMessage.addExtension(new ChatStateExtension(ChatState.active)); - - if (encrypted) { - Optional encryptedData = content.isComplex() || chat.isGroupChat() ? - Coder.encryptStanza(message, - rawMessage(content, chat, true).toXML().toString()) : - Coder.encryptMessage(message); - // check also for security errors just to be sure - if (!encryptedData.isPresent() || - !message.getCoderStatus().getErrors().isEmpty()) { - LOGGER.warning("encryption failed"); - message.setStatus(Status.ERROR); - mControl.handleSecurityErrors(message); - return false; - } - protoMessage.addExtension(new E2EEncryption(encryptedData.get())); - } - - // transmission specific - Transmission[] transmissions = message.getTransmissions(); - ArrayList sendMessages = new ArrayList<>(transmissions.length); - for (Transmission transmission: message.getTransmissions()) { - Message sendMessage = protoMessage.clone(); - JID to = transmission.getJID(); - if (!to.isValid()) { - LOGGER.warning("invalid JID: "+to); - return false; - } - sendMessage.setTo(to.string()); - sendMessages.add(sendMessage); - } - - return this.sendPackets(sendMessages.toArray(new Message[0])); - } - - private static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { - Message smackMessage = new Message(); - - // text - String text = content.getPlainText(); - if (!text.isEmpty()) - smackMessage.setBody(content.getPlainText()); - - // attachment - Optional optAtt = content.getAttachment(); - if (optAtt.isPresent()) { - Attachment att = optAtt.get(); - - OutOfBandData oobData = new OutOfBandData(att.getURL().toString(), - att.getMimeType(), att.getLength(), encrypted); - smackMessage.addExtension(oobData); - - Optional optPreview = content.getPreview(); - if (optPreview.isPresent()) { - Preview preview = optPreview.get(); - String data = EncodingUtils.bytesToBase64(preview.getData()); - BitsOfBinary bob = new BitsOfBinary(preview.getMimeType(), data); - smackMessage.addExtension(bob); - } - } - - // group command - if (chat instanceof GroupChat) { - GroupChat groupChat = (GroupChat) chat; - GID gid = groupChat.getGID(); - Optional optGroupCommand = content.getGroupCommand(); - smackMessage.addExtension(optGroupCommand.isPresent() ? - ClientUtils.groupCommandToGroupExtension(groupChat, optGroupCommand.get()) : - new GroupExtension(gid.id, gid.ownerJID.string())); - } - - return smackMessage; + return mMessageSender.sendMessage(message, sendChatState); } // TODO unused @@ -396,7 +283,7 @@ public void sendChatState(JID jid, String threadID, ChatState state) { this.sendPacket(message); } - private synchronized boolean sendPackets(Stanza[] stanzas) { + synchronized boolean sendPackets(Stanza[] stanzas) { boolean sent = true; for (Stanza s: stanzas) sent &= this.sendPacket(s); @@ -506,10 +393,6 @@ public void run() { } } - public boolean isConnected() { - return mConn != null && mConn.isAuthenticated(); - } - private static class Task { final Command command; diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java new file mode 100644 index 00000000..c4d23256 --- /dev/null +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -0,0 +1,169 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.logging.Logger; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.chatstates.ChatState; +import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; +import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; +import org.kontalk.crypto.Coder; +import org.kontalk.misc.JID; +import org.kontalk.model.Chat; +import org.kontalk.model.GroupChat; +import org.kontalk.model.KonMessage; +import org.kontalk.model.MessageContent; +import org.kontalk.model.OutMessage; +import org.kontalk.model.Transmission; +import org.kontalk.system.Control; +import org.kontalk.util.ClientUtils; +import org.kontalk.util.EncodingUtils; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +final class KonMessageSender { + private static final Logger LOGGER = Logger.getLogger(KonMessageSender.class.getName()); + + private final Client mClient; + private final Control mControl; + + KonMessageSender(Client client, Control control) { + mClient = client; + mControl = control; + } + + boolean sendMessage(OutMessage message, boolean sendChatState) { + // check for correct receipt status and reset it + KonMessage.Status status = message.getStatus(); + assert status == KonMessage.Status.PENDING || status == KonMessage.Status.ERROR; + message.setStatus(KonMessage.Status.PENDING); + + if (!mClient.isConnected()) { + LOGGER.info("not sending message(s), not connected"); + return false; + } + + MessageContent content = message.getContent(); + Optional optAtt = content.getAttachment(); + if (optAtt.isPresent() && !optAtt.get().hasURL()) { + LOGGER.warning("attachment not uploaded"); + message.setStatus(KonMessage.Status.ERROR); + return false; + } + + boolean encrypted = + message.getCoderStatus().getEncryption() != Coder.Encryption.NOT || + message.getCoderStatus().getSigning() != Coder.Signing.NOT; + + Chat chat = message.getChat(); + + Message protoMessage = encrypted ? new Message() : rawMessage(content, chat, false); + + protoMessage.setType(Message.Type.chat); + protoMessage.setStanzaId(message.getXMPPID()); + String threadID = chat.getXMPPID(); + if (!threadID.isEmpty()) + protoMessage.setThread(threadID); + + // extensions + + // TODO with group chat? (for muc "NOT RECOMMENDED") + if (!chat.isGroupChat()) + protoMessage.addExtension(new DeliveryReceiptRequest()); + + if (sendChatState) + protoMessage.addExtension(new ChatStateExtension(ChatState.active)); + + if (encrypted) { + Optional encryptedData = content.isComplex() || chat.isGroupChat() ? + Coder.encryptStanza(message, + rawMessage(content, chat, true).toXML().toString()) : + Coder.encryptMessage(message); + // check also for security errors just to be sure + if (!encryptedData.isPresent() || + !message.getCoderStatus().getErrors().isEmpty()) { + LOGGER.warning("encryption failed"); + message.setStatus(KonMessage.Status.ERROR); + mControl.handleSecurityErrors(message); + return false; + } + protoMessage.addExtension(new E2EEncryption(encryptedData.get())); + } + + // transmission specific + Transmission[] transmissions = message.getTransmissions(); + ArrayList sendMessages = new ArrayList<>(transmissions.length); + for (Transmission transmission: message.getTransmissions()) { + Message sendMessage = protoMessage.clone(); + JID to = transmission.getJID(); + if (!to.isValid()) { + LOGGER.warning("invalid JID: "+to); + return false; + } + sendMessage.setTo(to.string()); + sendMessages.add(sendMessage); + } + + //return mClient.sendPackets(sendMessages.toArray(new Message[0])); + return true; + } + + private static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { + Message smackMessage = new Message(); + + // text + String text = content.getPlainText(); + if (!text.isEmpty()) + smackMessage.setBody(content.getPlainText()); + + // attachment + Optional optAtt = content.getAttachment(); + if (optAtt.isPresent()) { + MessageContent.Attachment att = optAtt.get(); + + OutOfBandData oobData = new OutOfBandData(att.getURL().toString(), + att.getMimeType(), att.getLength(), encrypted); + smackMessage.addExtension(oobData); + + Optional optPreview = content.getPreview(); + if (optPreview.isPresent()) { + MessageContent.Preview preview = optPreview.get(); + String data = EncodingUtils.bytesToBase64(preview.getData()); + BitsOfBinary bob = new BitsOfBinary(preview.getMimeType(), data); + smackMessage.addExtension(bob); + } + } + + // group command + if (chat instanceof GroupChat) { + GroupChat groupChat = (GroupChat) chat; + GroupChat.GID gid = groupChat.getGID(); + Optional optGroupCommand = content.getGroupCommand(); + smackMessage.addExtension(optGroupCommand.isPresent() ? + ClientUtils.groupCommandToGroupExtension(groupChat, optGroupCommand.get()) : + new GroupExtension(gid.id, gid.ownerJID.string())); + } + + return smackMessage; + } +} From 61bcfcea8107b873d2dd585e28b6b64b4b147e07 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 18 Nov 2015 15:40:47 +0100 Subject: [PATCH 003/257] client: new receiver class for private key transmission --- src/main/java/org/kontalk/client/Client.java | 45 +++-- .../org/kontalk/client/EndpointServer.java | 7 + .../kontalk/client/PrivateKeyReceiver.java | 156 ++++++++++++++++++ src/main/java/org/kontalk/misc/Callback.java | 49 ++++++ 4 files changed, 231 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/kontalk/client/PrivateKeyReceiver.java create mode 100644 src/main/java/org/kontalk/misc/Callback.java diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index d862cc10..8531f5e4 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -83,11 +83,6 @@ public Client(Control control) { //SmackConfiguration.DEBUG = true; } - /** Connect to server without logging in. */ - public void connect() { - this.connect(null); - } - public void connect(PersonalKey key) { this.disconnect(); @@ -95,18 +90,15 @@ public void connect(PersonalKey key) { mControl.setStatus(Control.Status.CONNECTING); Config config = Config.getInstance(); - // tigase: use hostname as network //String network = config.getString(KonConf.SERV_NET); - String network = config.getString(Config.SERV_HOST); String host = config.getString(Config.SERV_HOST); int port = config.getInt(Config.SERV_PORT); - EndpointServer server = new EndpointServer(network, host, port); + EndpointServer server = new EndpointServer(host, port); + boolean validateCertificate = config.getBoolean(Config.SERV_CERT_VALIDATION); // create connection - mConn = key == null ? - new KonConnection(server, validateCertificate) : - new KonConnection(server, + mConn = new KonConnection(server, key.getServerLoginKey(), key.getBridgeCertificate(), validateCertificate); @@ -168,26 +160,22 @@ private void connectAsync() { return; } - if (mConn.hasLoginCredentials()) { - // login - try { - mConn.login(); - } catch (XMPPException | SmackException | IOException ex) { - LOGGER.log(Level.WARNING, "can't login on "+mConn.getServer(), ex); - mConn.disconnect(); - mControl.setStatus(Control.Status.FAILED); - mControl.handleException(new KonException(KonException.Error.CLIENT_LOGIN, ex)); - return; - } + // login + try { + mConn.login(); + } catch (XMPPException | SmackException | IOException ex) { + LOGGER.log(Level.WARNING, "can't login on "+mConn.getServer(), ex); + mConn.disconnect(); + mControl.setStatus(Control.Status.FAILED); + mControl.handleException(new KonException(KonException.Error.CLIENT_LOGIN, ex)); + return; } } mConn.addStanzaAcknowledgedListener(new AcknowledgedListener(mControl)); - if (mConn.isAuthenticated()) { - this.sendInitialPresence(); - this.sendBlocklistRequest(); - } + this.sendInitialPresence(); + this.sendBlocklistRequest(); mControl.setStatus(Control.Status.CONNECTED); } @@ -241,6 +229,11 @@ public void sendBlocklistRequest() { public void sendBlockingCommand(JID jid, boolean blocking) { LOGGER.info("jid: "+jid+" blocking="+blocking); + if (mConn == null || !this.isConnected()) { + LOGGER.warning("not connected"); + return; + } + String command = blocking ? BlockingCommand.BLOCK : BlockingCommand.UNBLOCK; BlockingCommand blockingCommand = new BlockingCommand(command, jid.string()); diff --git a/src/main/java/org/kontalk/client/EndpointServer.java b/src/main/java/org/kontalk/client/EndpointServer.java index 2c2338c1..9030d4fe 100644 --- a/src/main/java/org/kontalk/client/EndpointServer.java +++ b/src/main/java/org/kontalk/client/EndpointServer.java @@ -29,6 +29,13 @@ public final class EndpointServer { private final int mPort; private final String mNetwork; + public EndpointServer(String host, int port) { + // tigase: use hostname as network + mNetwork = host; + mHost = host; + mPort = port; + } + public EndpointServer(String network, String host, int port) { mNetwork = network; mHost = host; diff --git a/src/main/java/org/kontalk/client/PrivateKeyReceiver.java b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java new file mode 100644 index 00000000..bbb4c822 --- /dev/null +++ b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java @@ -0,0 +1,156 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smack.ExceptionCallback; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.iqregister.packet.Registration; +import org.jivesoftware.smackx.xdata.Form; +import org.jivesoftware.smackx.xdata.FormField; +import org.jivesoftware.smackx.xdata.packet.DataForm; +import org.kontalk.misc.Callback; + +/** + * Send request and listen to response for private data over + * 'jabber:iq:register' namespace. + * Temporary server connection is established on requesting. + * @author Alexander Bikadorov {@literal } + */ +public final class PrivateKeyReceiver implements StanzaListener { + private static final Logger LOGGER = Logger.getLogger(PrivateKeyReceiver.class.getName()); + + private static final String FORM_TYPE_VALUE = "http://kontalk.org/protocol/register#privatekey"; + private static final String FORM_TOKEN_VAR = "token"; + + private final Callback.Handler mHandler; + private KonConnection mConn = null; + + public PrivateKeyReceiver(Callback.Handler handler) { + mHandler = handler; + } + + public void sendRequest(EndpointServer server, boolean validateCertificate, + final String registrationToken) { + // create connection + mConn = new KonConnection(server, validateCertificate); + + new Thread() { + @Override + public void run() { + PrivateKeyReceiver.this.sendRequestAsync(registrationToken); + } + }.start(); + } + + private void sendRequestAsync(String registrationToken) { + // connect + try { + mConn.connect(); + } catch (XMPPException | SmackException | IOException ex) { + LOGGER.log(Level.WARNING, "can't connect to "+mConn.getServer(), ex); + mHandler.handle(new Callback(ex)); + return; + } + + Registration iq = new Registration(); + iq.setType(IQ.Type.set); + iq.setTo(mConn.getServiceName()); + Form form = new Form(DataForm.Type.submit); + + // form type field + FormField type = new FormField(FormField.FORM_TYPE); + type.setType(FormField.Type.hidden); + type.addValue(FORM_TYPE_VALUE); + form.addField(type); + + // token field + FormField fieldKey = new FormField(FORM_TOKEN_VAR); + fieldKey.setLabel("Registration token"); + fieldKey.setType(FormField.Type.text_single); + fieldKey.addValue(registrationToken); + form.addField(fieldKey); + + iq.addExtension(form.getDataFormToSend()); + + try { + mConn.sendIqWithResponseCallback(iq, this, new ExceptionCallback() { + @Override + public void processException(Exception exception) { + mHandler.handle(new Callback(exception)); + } + }); + } catch (SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "not connected", ex); + mHandler.handle(new Callback(ex)); + } + } + + @Override + public void processPacket(Stanza packet) { + LOGGER.info("response: "+packet); + + mConn.removeSyncStanzaListener(this); + mConn.disconnect(); + + if (!(packet instanceof IQ)) { + LOGGER.warning("response not an IQ packet"); + finish(null); + return; + } + IQ iq = (IQ) packet; + + if (iq.getType() != IQ.Type.result) { + LOGGER.warning("ignoring response with IQ type: "+iq.getType()); + this.finish(null); + return; + } + + DataForm response = iq.getExtension(DataForm.ELEMENT, DataForm.NAMESPACE); + if (response == null) { + this.finish(null); + return; + } + + String token = null; + List fields = response.getFields(); + for (FormField field : fields) { + if ("token".equals(field.getVariable())) { + token = field.getValues().get(0); + break; + } + } + + this.finish(token); + } + + private void finish(String token) { + mHandler.handle(token == null ? + new Callback() : + new Callback<>(token)); + } + +} diff --git a/src/main/java/org/kontalk/misc/Callback.java b/src/main/java/org/kontalk/misc/Callback.java new file mode 100644 index 00000000..b2c4b9fd --- /dev/null +++ b/src/main/java/org/kontalk/misc/Callback.java @@ -0,0 +1,49 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.misc; + +import java.util.Optional; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +public final class Callback { + public final V value; + public final Optional exception; + + public Callback() { + this.value = null; + this.exception = Optional.empty(); + } + + public Callback(V response) { + this.value = response; + this.exception = Optional.empty(); + } + + public Callback(Exception ex) { + this.value = null; + this.exception = Optional.of(ex); + } + + public interface Handler { + void handle(Callback callback); + } +} From 4c236b356e915df14672da5ec0ea6d465012efd6 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 18 Nov 2015 17:30:01 +0100 Subject: [PATCH 004/257] system: splitted account and account importing + added importing with private key transfer (unused, untested) --- src/main/java/org/kontalk/Kontalk.java | 4 +- src/main/java/org/kontalk/crypto/Coder.java | 4 +- .../{AccountLoader.java => Account.java} | 50 +++---- .../org/kontalk/system/AccountImporter.java | 127 ++++++++++++++++++ .../org/kontalk/system/AttachmentManager.java | 2 +- src/main/java/org/kontalk/system/Control.java | 4 +- .../org/kontalk/view/ConfigurationDialog.java | 10 +- .../java/org/kontalk/view/ImportDialog.java | 92 ++++++++++--- 8 files changed, 226 insertions(+), 67 deletions(-) rename src/main/java/org/kontalk/system/{AccountLoader.java => Account.java} (82%) create mode 100644 src/main/java/org/kontalk/system/AccountImporter.java diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 17e1b997..16529677 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -36,7 +36,7 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.model.ChatList; import org.kontalk.model.ContactList; -import org.kontalk.system.AccountLoader; +import org.kontalk.system.Account; import org.kontalk.system.Config; import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; @@ -123,7 +123,7 @@ private void start() { Config.initialize(mAppDir.resolve(Config.FILENAME)); - AccountLoader.initialize(mAppDir); + Account.initialize(mAppDir); ViewControl control = Control.create(mAppDir); diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index de994f70..b0ed6548 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -29,7 +29,7 @@ import org.kontalk.model.OutMessage; import org.kontalk.model.DecryptMessage; import org.kontalk.model.InMessage; -import org.kontalk.system.AccountLoader; +import org.kontalk.system.Account; /** * Static methods for decryption and encryption of a message. @@ -89,7 +89,7 @@ public static enum Error { private static final HashMap KEY_MAP = new HashMap<>(); static PersonalKey myKeyOrNull() { - Optional optMyKey = AccountLoader.getInstance().getPersonalKey(); + Optional optMyKey = Account.getInstance().getPersonalKey(); if (!optMyKey.isPresent()) { LOGGER.log(Level.WARNING, "can't get personal key"); return null; diff --git a/src/main/java/org/kontalk/system/AccountLoader.java b/src/main/java/org/kontalk/system/Account.java similarity index 82% rename from src/main/java/org/kontalk/system/AccountLoader.java rename to src/main/java/org/kontalk/system/Account.java index 525977c5..f221d0ca 100644 --- a/src/main/java/org/kontalk/system/AccountLoader.java +++ b/src/main/java/org/kontalk/system/Account.java @@ -30,9 +30,6 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import org.apache.commons.io.IOUtils; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.jivesoftware.smack.util.StringUtils; @@ -41,20 +38,24 @@ import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; -public final class AccountLoader { - private static final Logger LOGGER = Logger.getLogger(AccountLoader.class.getName()); +/** + * The user account. There can only be one. + * @author Alexander Bikadorov {@literal } + */ +public final class Account { + private static final Logger LOGGER = Logger.getLogger(Account.class.getName()); private static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; private static final String BRIDGE_CERT_FILENAME = "kontalk-login.crt"; - private static AccountLoader INSTANCE = null; + private static Account INSTANCE = null; private final Path mKeyDir; private final Config mConf; private PersonalKey mKey = null; - private AccountLoader(Path keyDir, Config config) { + private Account(Path keyDir, Config config) { mKeyDir = keyDir; mConf = config; } @@ -80,24 +81,13 @@ PersonalKey load(char[] password) throws KonException { return mKey; } - public void importAccount(String zipFilePath, char[] password) throws KonException { - byte[] privateKeyData; - - // read key files - try (ZipFile zipFile = new ZipFile(zipFilePath)) { - privateKeyData = AccountLoader.readBytesFromZip(zipFile, PRIVATE_KEY_FILENAME); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't open zip archive: ", ex); - throw new KonException(KonException.Error.IMPORT_ARCHIVE, ex); - } - + void setAccount(byte[] privateKeyData, char[] password) throws KonException { // try to load key PersonalKey key; byte[] encodedPrivateKey; try { - encodedPrivateKey = PGPUtils.disarm(privateKeyData); - key = PersonalKey.load(encodedPrivateKey, - password); + encodedPrivateKey = privateKeyData; + key = PersonalKey.load(encodedPrivateKey, password); } catch (PGPException | IOException | CertificateException | NoSuchProviderException ex) { LOGGER.log(Level.WARNING, "can't import personal key", ex); @@ -122,6 +112,8 @@ public void importAccount(String zipFilePath, char[] password) throws KonExcepti // overwritten when connecting to server String address = PGPUtils.parseUID(key.getUserId())[2]; Config.getInstance().setProperty(Config.ACC_JID, address); + + LOGGER.info("new account, temporary JID: "+address); } public void setPassword(char[] oldPassword, char[] newPassword) throws KonException { @@ -204,27 +196,15 @@ private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } - private static byte[] readBytesFromZip(ZipFile zipFile, String filename) throws KonException { - ZipEntry zipEntry = zipFile.getEntry(filename); - byte[] bytes = null; - try { - bytes = IOUtils.toByteArray(zipFile.getInputStream(zipEntry)); - } catch (IOException ex) { - LOGGER.warning("can't read key file from archive: "+ex.getLocalizedMessage()); - throw new KonException(KonException.Error.IMPORT_READ_FILE, ex); - } - return bytes; - } - public synchronized static void initialize(Path keyDir) { if (INSTANCE != null) { LOGGER.warning("account loader already initialized"); return; } - INSTANCE = new AccountLoader(keyDir, Config.getInstance()); + INSTANCE = new Account(keyDir, Config.getInstance()); } - public synchronized static AccountLoader getInstance() { + public synchronized static Account getInstance() { if (INSTANCE == null) throw new IllegalStateException("account loader not initialized"); return INSTANCE; diff --git a/src/main/java/org/kontalk/system/AccountImporter.java b/src/main/java/org/kontalk/system/AccountImporter.java new file mode 100644 index 00000000..5568a943 --- /dev/null +++ b/src/main/java/org/kontalk/system/AccountImporter.java @@ -0,0 +1,127 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.system; + +import java.io.IOException; +import java.util.Observable; +import java.util.Observer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.apache.commons.io.IOUtils; +import org.kontalk.client.EndpointServer; +import org.kontalk.client.PrivateKeyReceiver; +import org.kontalk.crypto.PGPUtils; +import org.kontalk.misc.Callback; +import org.kontalk.misc.KonException; +import org.kontalk.util.EncodingUtils; + +/** + * Import and set user account from various sources. + * @author Alexander Bikadorov {@literal } + */ +public final class AccountImporter extends Observable implements Callback.Handler{ + private static final Logger LOGGER = Logger.getLogger(AccountImporter.class.getName()); + + static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; + + private char[] mPassword = null; + private boolean mAborted = false; + + public AccountImporter(Observer o) { + this.addObserver(o); + } + + public void fromZipFile(String zipFilePath, char[] password) { + // read key files + byte[] privateKeyData; + try (ZipFile zipFile = new ZipFile(zipFilePath)) { + privateKeyData = readBytesFromZip(zipFile, PRIVATE_KEY_FILENAME); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't open zip archive: ", ex); + this.changed(new KonException(KonException.Error.IMPORT_ARCHIVE, ex)); + return; + } catch (KonException ex) { + this.changed(ex); + return; + } + + this.set(privateKeyData, password); + } + + // note: with disarming + private static byte[] readBytesFromZip(ZipFile zipFile, String filename) throws KonException { + ZipEntry zipEntry = zipFile.getEntry(filename); + byte[] bytes = null; + try { + bytes = PGPUtils.disarm(IOUtils.toByteArray(zipFile.getInputStream(zipEntry))); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't read key file from archive: ", ex); + throw new KonException(KonException.Error.IMPORT_READ_FILE, ex); + } + return bytes; + } + + public void fromServer(String host, int port, boolean validateCertificate, + String token, char[] password) { + mPassword = password; + // send private key request + EndpointServer server = new EndpointServer(host, port); + PrivateKeyReceiver receiver = new PrivateKeyReceiver(this); + mAborted = false; + receiver.sendRequest(server, validateCertificate, token); + + // wait for response... continue with handle callback + } + + public void abort() { + // receiver will always terminate after some time, just ignore response + mAborted = true; + } + + @Override + public void handle(Callback callback) { + if (mAborted) + return; + + if (callback.exception.isPresent()) { + this.changed(callback.exception); + return; + } + + this.set(EncodingUtils.base64ToBytes(callback.value), mPassword); + } + + private void set(byte[] privateKeyData, char[] password) { + try { + Account.getInstance().setAccount(privateKeyData, password); + } catch (KonException ex) { + this.changed(ex); + return; + } + // report success + this.changed(null); + } + + private void changed(Object arg) { + this.setChanged(); + this.notifyObservers(arg); + } +} diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index f3433754..062d4dec 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -376,7 +376,7 @@ static Attachment attachmentOrNull(Path path) { } private static HTTPFileClient createClientOrNull(){ - Optional optKey = AccountLoader.getInstance().getPersonalKey(); + Optional optKey = Account.getInstance().getPersonalKey(); if (!optKey.isPresent()) { LOGGER.log(Level.WARNING, "personal key not loaded"); return null; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index efcf1cd9..1336c876 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -516,7 +516,7 @@ public void launch() { new Thread(mClient).start(); boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); - if (!AccountLoader.getInstance().accountIsPresent()) { + if (!Account.getInstance().accountIsPresent()) { LOGGER.info("no account found, asking for import..."); this.changed(new ViewEvent.MissingAccount(connect)); return; @@ -726,7 +726,7 @@ private void sendTextMessage(Chat chat, String text, Path file) { } private PersonalKey keyOrNull(char[] password) { - AccountLoader account = AccountLoader.getInstance(); + Account account = Account.getInstance(); Optional optKey = account.getPersonalKey(); if (optKey.isPresent()) return optKey.get(); diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 562fe999..38fdef1c 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -51,7 +51,7 @@ import org.kontalk.system.Config; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; -import org.kontalk.system.AccountLoader; +import org.kontalk.system.Account; import org.kontalk.util.Tr; /** @@ -279,7 +279,7 @@ public void actionPerformed(ActionEvent e) { } private void updateFingerprint() { - Optional optKey = AccountLoader.getInstance().getPersonalKey(); + Optional optKey = Account.getInstance().getPersonalKey(); mFingerprintArea.setText(optKey.isPresent() ? Utils.fingerprint(optKey.get().getFingerprint()) : "- " + Tr.tr("no key loaded") + " -"); @@ -335,7 +335,7 @@ private static WebCheckBox createCheckBox(String title, String tooltip, boolean } private static String getPassTitle() { - return AccountLoader.getInstance().isPasswordProtected() ? + return Account.getInstance().isPasswordProtected() ? Tr.tr("Change key password") : Tr.tr("Set key password"); } @@ -347,7 +347,7 @@ private static WebDialog createPassDialog(WebDialog parent) { final WebButton saveButton = new WebButton(Tr.tr("Save")); - boolean passSet = AccountLoader.getInstance().isPasswordProtected(); + boolean passSet = Account.getInstance().isPasswordProtected(); final ComponentUtils.PassPanel passPanel = new ComponentUtils.PassPanel(passSet) { @Override void onValidInput() { @@ -371,7 +371,7 @@ public void actionPerformed(ActionEvent e) { } char[] newPassword = optNewPass.get(); try { - AccountLoader.getInstance().setPassword(oldPassword, newPassword); + Account.getInstance().setPassword(oldPassword, newPassword); } catch(KonException ex) { LOGGER.log(Level.WARNING, "can't set new password", ex); if (ex.getError() == KonException.Error.CHANGE_PASS_COPY) diff --git a/src/main/java/org/kontalk/view/ImportDialog.java b/src/main/java/org/kontalk/view/ImportDialog.java index 09e48f2f..cb71e318 100644 --- a/src/main/java/org/kontalk/view/ImportDialog.java +++ b/src/main/java/org/kontalk/view/ImportDialog.java @@ -39,13 +39,17 @@ import java.io.File; import java.util.EnumMap; import java.util.List; +import java.util.Observable; +import java.util.Observer; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.filechooser.FileNameExtensionFilter; import org.kontalk.misc.KonException; -import org.kontalk.system.AccountLoader; +import org.kontalk.system.AccountImporter; +import org.kontalk.system.Account; import org.kontalk.util.Tr; /** @@ -67,6 +71,8 @@ private static enum Direction {BACK, FORTH}; private final View mView; private final boolean mConnect; + private final ResultPanel mResultPanel; + private ImportPage mCurrentPage; // exchanged between panels @@ -90,6 +96,7 @@ private static enum Direction {BACK, FORTH}; mBackButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + mResultPanel.mayAbort(); ImportDialog.this.switchPage(Direction.BACK); } }); @@ -104,6 +111,7 @@ public void actionPerformed(ActionEvent e) { mCancelButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { + mResultPanel.mayAbort(); ImportDialog.this.dispose(); } }); @@ -125,7 +133,8 @@ public void actionPerformed(ActionEvent e) { mPanels = new EnumMap<>(ImportPage.class); mPanels.put(ImportPage.INTRO, new IntroPanel()); mPanels.put(ImportPage.SETTINGS, new SettingsPanel()); - mPanels.put(ImportPage.RESULT, new ResultPanel()); + mResultPanel = new ResultPanel(); + mPanels.put(ImportPage.RESULT, mResultPanel); this.setPage(ImportPage.INTRO); } @@ -257,13 +266,19 @@ protected void onNext() { } } - private class ResultPanel extends ImportPanel { + private class ResultPanel extends ImportPanel implements Observer { + + private final AccountImporter mImporter; private final WebLabel mResultLabel; private final WebLabel mErrorLabel; private final ComponentUtils.PassPanel mPassPanel; + private boolean mWaiting = false; + ResultPanel() { + mImporter = new AccountImporter(this); + GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); groupPanel.setMargin(View.MARGIN_BIG); @@ -290,37 +305,74 @@ void onInvalidInput() { this.add(groupPanel); } - private boolean importAccount() { + @Override + protected void onShow() { + mNextButton.setVisible(false); + mCancelButton.setVisible(true); + this.importAccount(); + } + + private void importAccount() { if (mZipPath.isEmpty()) { LOGGER.warning("no zip file path"); - return false; + return; + } + + mResultLabel.setText(Tr.tr("Waiting...")); + mWaiting = true; + mImporter.fromZipFile(mZipPath, mPasswd); + } + + @Override + public void update(Observable o, final Object arg) { + if (SwingUtilities.isEventDispatchThread()) { + this.updateOnEDT(arg); + return; + } + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + ResultPanel.this.updateOnEDT(arg); + } + }); + } + + private void updateOnEDT(Object arg) { + if (arg == null) { + this.onResult(null); + } else if (arg instanceof KonException) { + this.onResult((KonException) arg); + } else { + LOGGER.warning("unexpected argument: "+arg); } + } + + private void onResult(KonException ex) { + mWaiting = false; String errorText = null; - try { - AccountLoader.getInstance().importAccount(mZipPath, mPasswd); - } catch (KonException ex) { + if (ex != null) { errorText = Utils.getErrorText(ex); + } else { + mCancelButton.setVisible(false); + mFinishButton.setVisible(true); } - mPassPanel.setVisible(errorText == null); + mPassPanel.setVisible(ex == null); - String result = errorText == null ? Tr.tr("Success!") : Tr.tr("Error"); + String result = ex == null ? Tr.tr("Success!") : Tr.tr("Error"); mResultLabel.setText(Tr.tr("Import process finished with:")+" "+result); mErrorLabel.setText(errorText == null ? "" : ""+Tr.tr("Error description:")+" \n\n"+errorText+""); - return errorText == null; } - @Override - protected void onShow() { - mNextButton.setVisible(false); - boolean success = this.importAccount(); - if (success) { - mCancelButton.setVisible(false); - mFinishButton.setVisible(true); - } + private void mayAbort() { + if (!mWaiting) + return; + + mImporter.abort(); + mWaiting = false; } @Override @@ -328,7 +380,7 @@ protected void onNext() { Optional optNewPass = mPassPanel.getNewPassword(); if (optNewPass.isPresent() && optNewPass.get().length > 0) { try { - AccountLoader.getInstance().setPassword(new char[0], optNewPass.get()); + Account.getInstance().setPassword(new char[0], optNewPass.get()); } catch (KonException ex) { LOGGER.log(Level.WARNING, "can't set password", ex); return; From 619e4321ac1b50582965d76bc9e781059fab373c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 18 Nov 2015 17:32:46 +0100 Subject: [PATCH 005/257] crypto: more output on import errors --- src/main/java/org/kontalk/crypto/PGPUtils.java | 1 + src/main/java/org/kontalk/crypto/PersonalKey.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/crypto/PGPUtils.java b/src/main/java/org/kontalk/crypto/PGPUtils.java index 54bd6914..b8def80e 100644 --- a/src/main/java/org/kontalk/crypto/PGPUtils.java +++ b/src/main/java/org/kontalk/crypto/PGPUtils.java @@ -218,6 +218,7 @@ static PGPKeyPair decrypt(PGPSecretKey secretKey, PBESecretKeyDecryptor dec) thr try { return new PGPKeyPair(secretKey.getPublicKey(), secretKey.extractPrivateKey(dec)); } catch (PGPException ex) { + LOGGER.log(Level.WARNING, "failed", ex); throw new KonException(KonException.Error.LOAD_KEY_DECRYPT, ex); } } diff --git a/src/main/java/org/kontalk/crypto/PersonalKey.java b/src/main/java/org/kontalk/crypto/PersonalKey.java index 80aa5ba2..3e845e94 100644 --- a/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -148,9 +148,12 @@ public static PersonalKey load(byte[] privateKeyData, signKey = authKey; } - if (authKey == null || signKey == null || encrKey == null) + if (authKey == null || signKey == null || encrKey == null) { + LOGGER.warning("something could not be found, " + +"sign="+signKey+ ", auth="+authKey+", encr="+encrKey); throw new KonException(KonException.Error.LOAD_KEY, new PGPException("could not find all keys in key data")); + } // decrypt private keys PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder( From cdf3ec8dfeb544c48f3952520defe2b628973631 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 18 Nov 2015 18:04:21 +0100 Subject: [PATCH 006/257] crypto: simplified disarming --- .../java/org/kontalk/crypto/PGPUtils.java | 5 ++-- src/main/java/org/kontalk/system/Account.java | 27 +++++++------------ .../org/kontalk/system/AccountImporter.java | 5 ++-- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/PGPUtils.java b/src/main/java/org/kontalk/crypto/PGPUtils.java index b8def80e..cb382c14 100644 --- a/src/main/java/org/kontalk/crypto/PGPUtils.java +++ b/src/main/java/org/kontalk/crypto/PGPUtils.java @@ -36,7 +36,6 @@ import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; -import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedData; @@ -110,8 +109,8 @@ public static void registerProvider() { Security.insertProviderAt(new BouncyCastleProvider(), 1); } - public static byte[] disarm(byte[] key) throws IOException { - return IOUtils.toByteArray(new ArmoredInputStream(new ByteArrayInputStream(key))); + public static byte[] mayDisarm(InputStream input) throws IOException { + return IOUtils.toByteArray(PGPUtil.getDecoderStream(input)); } /** diff --git a/src/main/java/org/kontalk/system/Account.java b/src/main/java/org/kontalk/system/Account.java index f221d0ca..638c7d7f 100644 --- a/src/main/java/org/kontalk/system/Account.java +++ b/src/main/java/org/kontalk/system/Account.java @@ -19,10 +19,11 @@ package org.kontalk.system; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; -import java.nio.file.Files; import java.nio.file.Path; import java.security.NoSuchProviderException; import java.security.cert.CertificateEncodingException; @@ -30,6 +31,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.io.IOUtils; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.jivesoftware.smack.util.StringUtils; @@ -66,8 +68,8 @@ public Optional getPersonalKey() { PersonalKey load(char[] password) throws KonException { // read key files - byte[] privateKeyData = this.readArmoredFile(PRIVATE_KEY_FILENAME); - byte[] bridgeCertData = this.readFile(BRIDGE_CERT_FILENAME); + byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); + byte[] bridgeCertData = this.readFile(BRIDGE_CERT_FILENAME, false); // load key try { @@ -117,7 +119,7 @@ void setAccount(byte[] privateKeyData, char[] password) throws KonException { } public void setPassword(char[] oldPassword, char[] newPassword) throws KonException { - byte[] privateKeyData = this.readArmoredFile(PRIVATE_KEY_FILENAME); + byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); this.writePrivateKey(privateKeyData, oldPassword, newPassword); } @@ -159,21 +161,12 @@ public boolean isPasswordProtected() { return mConf.getString(Config.ACC_PASS).isEmpty(); } - private byte[] readArmoredFile(String filename) throws KonException { - try { - return PGPUtils.disarm(this.readFile(filename)); - } catch (IOException ex) { - LOGGER.warning("can't read armored key file: "+ex.getLocalizedMessage()); - throw new KonException(KonException.Error.READ_FILE, ex); - } - } - - private byte[] readFile(String filename) throws KonException { + private byte[] readFile(String filename, boolean disarm) throws KonException { byte[] bytes = null; - try { - bytes = Files.readAllBytes(new File(mKeyDir.toString(), filename).toPath()); + try (InputStream input = new FileInputStream(new File(mKeyDir.toString(), filename))) { + bytes = disarm ? PGPUtils.mayDisarm(input) : IOUtils.toByteArray(input); } catch (IOException ex) { - LOGGER.warning("can't read key file: "+ex.getLocalizedMessage()); + LOGGER.log(Level.WARNING, "can't read key file", ex); throw new KonException(KonException.Error.READ_FILE, ex); } return bytes; diff --git a/src/main/java/org/kontalk/system/AccountImporter.java b/src/main/java/org/kontalk/system/AccountImporter.java index 5568a943..dea33d12 100644 --- a/src/main/java/org/kontalk/system/AccountImporter.java +++ b/src/main/java/org/kontalk/system/AccountImporter.java @@ -25,7 +25,6 @@ import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import org.apache.commons.io.IOUtils; import org.kontalk.client.EndpointServer; import org.kontalk.client.PrivateKeyReceiver; import org.kontalk.crypto.PGPUtils; @@ -66,12 +65,12 @@ public void fromZipFile(String zipFilePath, char[] password) { this.set(privateKeyData, password); } - // note: with disarming + // note: with disarming if needed private static byte[] readBytesFromZip(ZipFile zipFile, String filename) throws KonException { ZipEntry zipEntry = zipFile.getEntry(filename); byte[] bytes = null; try { - bytes = PGPUtils.disarm(IOUtils.toByteArray(zipFile.getInputStream(zipEntry))); + bytes = PGPUtils.mayDisarm(zipFile.getInputStream(zipEntry)); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't read key file from archive: ", ex); throw new KonException(KonException.Error.IMPORT_READ_FILE, ex); From 827e8e5ace78b0a81e0967337663c5226bd46c9f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 18 Nov 2015 18:51:26 +0100 Subject: [PATCH 007/257] translation: renamed simplified Chinese to just Chinese --- .../i18n/{strings_zh_Hans.properties => strings_zh.properties} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/i18n/{strings_zh_Hans.properties => strings_zh.properties} (100%) diff --git a/src/main/resources/i18n/strings_zh_Hans.properties b/src/main/resources/i18n/strings_zh.properties similarity index 100% rename from src/main/resources/i18n/strings_zh_Hans.properties rename to src/main/resources/i18n/strings_zh.properties From 1203e53f1a44ae283f623e8e179a48f0a88c61ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Ne=C4=8Das?= Date: Wed, 18 Nov 2015 20:47:00 +0000 Subject: [PATCH 008/257] Translated using Weblate (Czech) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_cs.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/i18n/strings_cs.properties b/src/main/resources/i18n/strings_cs.properties index 72902c49..c288e049 100644 --- a/src/main/resources/i18n/strings_cs.properties +++ b/src/main/resources/i18n/strings_cs.properties @@ -260,3 +260,4 @@ s_PC1G=píše… s_KRQ6=Zadejte heslo… s_2R2P=načítání… s_307W=stahování… +s_4WC0=Nepodařilo se ověřit certifikát serveru. From fa1a55b70a60439bc93b7e6344f008e34d7c7565 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 21 Nov 2015 19:18:18 +0100 Subject: [PATCH 009/257] view: arguments for table cell renderer can be null, fixes #58 --- src/main/java/org/kontalk/view/Table.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/Table.java b/src/main/java/org/kontalk/view/Table.java index b4e84a94..053636e3 100644 --- a/src/main/java/org/kontalk/view/Table.java +++ b/src/main/java/org/kontalk/view/Table.java @@ -357,6 +357,7 @@ public String getToolTipText(MouseEvent event) { private class TableRenderer extends WebTableCellRenderer { // return for each item (value) in the list/table the component to // render - which is the item itself here + // NOTE: table and value can be NULL @Override @SuppressWarnings("unchecked") public Component getTableCellRendererComponent(JTable table, @@ -366,6 +367,9 @@ public Component getTableCellRendererComponent(JTable table, int row, int column) { TableItem item = (TableItem) value; + // hopefully return value is not used + if (table == null || item == null) + return item; item.render(table.getWidth(), isSelected); @@ -373,7 +377,7 @@ public Component getTableCellRendererComponent(JTable table, // view item needs a little more then it preferres height += 1; if (height != table.getRowHeight(row)) - // note: this calls resizeAndRepaint() + // NOTE: this calls resizeAndRepaint() table.setRowHeight(row, height); return item; } From 1cf88179defd0a50206a57030bfb90ad1d3cdd41 Mon Sep 17 00:00:00 2001 From: Naofumi Date: Sun, 22 Nov 2015 00:13:25 +0000 Subject: [PATCH 010/257] Translated using Weblate (Japanese) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_ja.properties | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/resources/i18n/strings_ja.properties b/src/main/resources/i18n/strings_ja.properties index af80d90c..8d5c6f0c 100644 --- a/src/main/resources/i18n/strings_ja.properties +++ b/src/main/resources/i18n/strings_ja.properties @@ -281,3 +281,13 @@ s_B5NX=グループチャットを編集 s_U5RT=このグループを作成しました s_5X07=このグループを離れました s_FWZZ=グループを変更しました +s_OOS3=接続中… +s_0WD8=切断中… +s_Z1GT=アーカイブからすべてのキーファイルを読み込みできません。 +s_MMBD=検索… +s_ZZWW=他のユーザーにチャットのアクティビティ (入力中、…) を送信 +s_PC1G=入力中… +s_KRQ6=パスワードを入力… +s_2R2P=読み込み中… +s_307W=ダウンロード中… +s_4WC0=サーバー証明書は検証されていません。 From cd6ab667c2d8831ecbb72bdeec3c3a8ddf825dce Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 27 Nov 2015 17:03:02 +0100 Subject: [PATCH 011/257] system: fix comparing contact with presence fingerprint --- src/main/java/org/kontalk/system/RosterHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index eae91895..2f515190 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -140,7 +140,8 @@ public void onFingerprintPresence(JID jid, String fingerprint) { } Contact contact = optContact.get(); - if (!contact.getFingerprint().equals(fingerprint)) { + if (!fingerprint.isEmpty() && + !fingerprint.equalsIgnoreCase(contact.getFingerprint())) { LOGGER.info("detected public key change, requesting new key..."); mControl.maySendKeyRequest(contact); } From a62857d9c1658ef3e920a6e41c56383fc442bd29 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 27 Nov 2015 17:54:51 +0100 Subject: [PATCH 012/257] use fingerprint always with lower case --- src/main/java/org/kontalk/client/PresenceListener.java | 2 +- src/main/java/org/kontalk/crypto/PersonalKey.java | 2 +- src/main/java/org/kontalk/model/Contact.java | 4 ++-- src/main/java/org/kontalk/view/Utils.java | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index 2bf6e644..2e2a56cb 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -93,7 +93,7 @@ public void processPacket(Stanza packet) { PublicKeyPresence.NAMESPACE); if (publicKeyExt instanceof PublicKeyPresence) { PublicKeyPresence pubKey = (PublicKeyPresence) publicKeyExt; - String fingerprint = StringUtils.defaultString(pubKey.getFingerprint()); + String fingerprint = StringUtils.defaultString(pubKey.getFingerprint()).toLowerCase(); if (!fingerprint.isEmpty()) { mHandler.onFingerprintPresence(jid, fingerprint); } else { diff --git a/src/main/java/org/kontalk/crypto/PersonalKey.java b/src/main/java/org/kontalk/crypto/PersonalKey.java index 3e845e94..6658e8a6 100644 --- a/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -104,7 +104,7 @@ public String getUserId() { } public String getFingerprint() { - return Hex.toHexString(mAuthKey.getFingerprint()).toUpperCase(); + return Hex.toHexString(mAuthKey.getFingerprint()); } /** Creates a {@link PersonalKey} from private keyring data. diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 2a32aa7f..fb9ddb4d 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -123,7 +123,7 @@ public static enum Subscription { mLastSeen = lastSeen; mEncrypted = encrypted; mKey = publicKey; - mFingerprint = fingerprint; + mFingerprint = fingerprint.toLowerCase(); } public JID getJID() { @@ -230,7 +230,7 @@ public void setKey(byte[] rawKey, String fingerprint) { LOGGER.info("overwriting public key of contact: "+this); mKey = EncodingUtils.bytesToBase64(rawKey); - mFingerprint = fingerprint; + mFingerprint = fingerprint.toLowerCase(); this.save(); this.changed(new byte[0]); } diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 5ee72b91..f0564f54 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -230,6 +230,7 @@ static String chatTitle(Chat chat) { } static String fingerprint(String fp) { + fp = fp.toUpperCase(); int m = fp.length() / 2; return group(fp.substring(0, m)) + "\n" + group(fp.substring(m)); } From 35fd69f219faff17970aec99a049420bc2d82a1c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 27 Nov 2015 17:56:00 +0100 Subject: [PATCH 013/257] view: set content view after contact was deleted --- src/main/java/org/kontalk/view/ContactListView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 9b0cdf9c..bca69e31 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -265,6 +265,7 @@ public void actionPerformed(ActionEvent event) { if (!Utils.confirmDeletion(ContactListView.this, text)) return; mView.getControl().deleteContact(mItem.mValue); + mView.showNothing(); } }); this.add(mDeleteMenuItem); From eaa8499dff363208d68684707d48c5b6ff4b41f7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 27 Nov 2015 17:59:36 +0100 Subject: [PATCH 014/257] model: create new contacts with bare JID --- src/main/java/org/kontalk/misc/JID.java | 4 ++++ src/main/java/org/kontalk/model/ContactList.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 17c850b8..cd4362af 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -71,6 +71,10 @@ public boolean isMe() { this.equals(JID.me()); } + public JID toBare() { + return new JID(mLocal, mDomain, ""); + } + /** * Comparing only bare JIDs. * Case-insensitive (local and domain part, resource is case-sensitive). diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 33b82bf6..e7636471 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -80,6 +80,8 @@ public void load() { * Create and add a new contact. */ public Optional create(JID jid, String name) { + jid = jid.toBare(); + if (!this.isValid(jid)) return Optional.empty(); From d9f2a3b31e2a1a52de5a7f2345ca3c63ea05f265 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 27 Nov 2015 19:07:01 +0100 Subject: [PATCH 015/257] client: activate sending messages again (since f7ca96b3 ) --- src/main/java/org/kontalk/client/KonMessageSender.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index c4d23256..5cbc28d1 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -124,8 +124,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { sendMessages.add(sendMessage); } - //return mClient.sendPackets(sendMessages.toArray(new Message[0])); - return true; + return mClient.sendPackets(sendMessages.toArray(new Message[0])); } private static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { From c50149bbb2a7ab90e69a2a4a72d39cd7871007df Mon Sep 17 00:00:00 2001 From: julio amoros Date: Sat, 28 Nov 2015 14:32:41 +0000 Subject: [PATCH 016/257] Translated using Weblate (Catalan) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_ca.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/i18n/strings_ca.properties b/src/main/resources/i18n/strings_ca.properties index 97c8713f..dc0dcdc0 100644 --- a/src/main/resources/i18n/strings_ca.properties +++ b/src/main/resources/i18n/strings_ca.properties @@ -425,3 +425,4 @@ s_PC1G=està escrivint… s_KRQ6=Introdueixi la contrasenya… s_2R2P=carregant… s_307W=baixant… +s_4WC0=El certificat del servidor no pot ser validat. From c5ae2daf9b1cc35cbfe3561a3a928859b550ee71 Mon Sep 17 00:00:00 2001 From: julio amoros Date: Sat, 28 Nov 2015 14:42:58 +0000 Subject: [PATCH 017/257] Translated using Weblate (Spanish) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_es.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/i18n/strings_es.properties b/src/main/resources/i18n/strings_es.properties index 38d49d06..4ced5a48 100644 --- a/src/main/resources/i18n/strings_es.properties +++ b/src/main/resources/i18n/strings_es.properties @@ -297,3 +297,4 @@ s_PC1G=está escribiendo… s_KRQ6=Introduzca la contraseña… s_2R2P=cargando… s_307W=bajando… +s_4WC0=No se pudo validar el certificado del servidor. From 580180137da32532f7fd6365d77316b7ca66fc67 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 28 Nov 2015 18:17:35 +0100 Subject: [PATCH 018/257] manually handle subscription requests --- src/main/java/org/kontalk/client/Client.java | 30 +++++++----- .../org/kontalk/client/PresenceListener.java | 49 ++++++++++++------- src/main/java/org/kontalk/system/Control.java | 38 ++++++++------ .../org/kontalk/system/RosterHandler.java | 16 ++++-- 4 files changed, 86 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 8531f5e4..708da97e 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -55,9 +55,6 @@ /** * Network client for an XMPP Kontalk Server. * - * Note: By default incoming presence subscription requests are automatically - * granted by Smack (but Kontalk uses a custom subscription request!?) - * * @author Alexander Bikadorov {@literal } */ public final class Client implements StanzaListener, Runnable { @@ -65,6 +62,8 @@ public final class Client implements StanzaListener, Runnable { private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); + public enum PresenceCommand {SUBSCRIBE, GRANT}; + private static enum Command {CONNECT, DISCONNECT}; private final Control mControl; @@ -106,10 +105,14 @@ public void connect(PersonalKey key) { // connection listener mConn.addConnectionListener(new KonConnectionListener(mControl)); + Roster roster = Roster.getInstanceFor(mConn); + // subscriptions handled by roster handler + roster.setSubscriptionMode(Roster.SubscriptionMode.manual); + // packet listeners RosterHandler rosterSyncer = mControl.getRosterHandler(); - RosterListener rl = new KonRosterListener(Roster.getInstanceFor(mConn), rosterSyncer); - Roster.getInstanceFor(mConn).addRosterListener(rl); + RosterListener rl = new KonRosterListener(roster, rosterSyncer); + roster.addRosterListener(rl); StanzaFilter messageFilter = new StanzaTypeFilter(Message.class); mConn.addAsyncStanzaListener(new KonMessageListener(this, mControl), messageFilter); @@ -124,7 +127,7 @@ public void connect(PersonalKey key) { mConn.addAsyncStanzaListener(new PublicKeyListener(mControl), publicKeyFilter); StanzaFilter presenceFilter = new StanzaTypeFilter(Presence.class); - mConn.addAsyncStanzaListener(new PresenceListener(Roster.getInstanceFor(mConn), rosterSyncer), presenceFilter); + mConn.addAsyncStanzaListener(new PresenceListener(roster, rosterSyncer), presenceFilter); // fallback listener mConn.addAsyncStanzaListener(this, @@ -260,11 +263,16 @@ public void sendInitialPresence() { this.sendPacket(presence); } - public void sendPresenceSubscriptionRequest(JID jid) { - LOGGER.info("to "+jid); - Presence subscribeRequest = new Presence(Presence.Type.subscribe); - subscribeRequest.setTo(jid.string()); - this.sendPacket(subscribeRequest); + public void sendPresenceSubscription(JID jid, PresenceCommand command) { + LOGGER.info("to "+jid+ " command: "+command); + Presence.Type type = null; + switch(command) { + case GRANT: type = Presence.Type.subscribed; break; + case SUBSCRIBE: type = Presence.Type.subscribe; break; + } + Presence presence = new Presence(type); + presence.setTo(jid.string()); + this.sendPacket(presence); } public void sendChatState(JID jid, String threadID, ChatState state) { diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index 2e2a56cb..8d6fa6c3 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -68,15 +68,34 @@ public void processPacket(Stanza packet) { JID jid = JID.bare(presence.getFrom()); - if (presence.getType() == Presence.Type.error) { - XMPPError error = presence.getError(); - if (error == null) { - LOGGER.warning("error presence does not contain error"); + ExtensionElement publicKeyExt = presence.getExtension( + PublicKeyPresence.ELEMENT_NAME, + PublicKeyPresence.NAMESPACE); + PublicKeyPresence pubKey = publicKeyExt instanceof PublicKeyPresence ? + (PublicKeyPresence) publicKeyExt : + null; + + switch(presence.getType()) { + case error: + XMPPError error = presence.getError(); + if (error == null) { + LOGGER.warning("error presence does not contain error"); + return; + } + mHandler.onPresenceError(jid, error.getType(), + error.getCondition()); + return; + // NOTE: only handled here if Roster.SubscriptionMode is set to 'manual' + case subscribe: + byte[] key = pubKey != null ? pubKey.getKey() : null; + if (key == null) + key = new byte[0]; + mHandler.onSubscriptionRequest(jid, key); + return; + case unsubscribe: + // TODO + LOGGER.info(("ignoring unsubscribe request, JID: "+jid)); return; - } - mHandler.onPresenceError(jid, error.getType(), - error.getCondition()); - return; } Presence bestPresence = mRoster.getPresence(jid.string()); @@ -88,16 +107,12 @@ public void processPacket(Stanza packet) { bestPresence.getType(), bestPresence.getStatus()); - ExtensionElement publicKeyExt = presence.getExtension( - PublicKeyPresence.ELEMENT_NAME, - PublicKeyPresence.NAMESPACE); - if (publicKeyExt instanceof PublicKeyPresence) { - PublicKeyPresence pubKey = (PublicKeyPresence) publicKeyExt; - String fingerprint = StringUtils.defaultString(pubKey.getFingerprint()).toLowerCase(); - if (!fingerprint.isEmpty()) { - mHandler.onFingerprintPresence(jid, fingerprint); - } else { + if (pubKey != null) { + String fp = StringUtils.defaultString(pubKey.getFingerprint()).toLowerCase(); + if (fp.isEmpty()) { LOGGER.warning("no fingerprint in public key presence extension"); + } else { + mHandler.onFingerprintPresence(jid, fp); } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 1336c876..69fa3a2b 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -155,10 +155,7 @@ public boolean newInMessage(MessageIDs ids, MessageContent content) { LOGGER.info("new incoming message, "+ids); - ContactList contactList = ContactList.getInstance(); - Optional optContact = contactList.contains(ids.jid) ? - contactList.get(ids.jid) : - this.createContact(ids.jid, ""); + Optional optContact = this.getOrCreate(ids.jid, ""); if (!optContact.isPresent()) { LOGGER.warning("can't get contact for message"); return false; @@ -267,8 +264,11 @@ public void handlePGPKey(JID jid, byte[] rawKey) { LOGGER.warning("can't find contact with jid: "+jid); return; } - Contact contact = optContact.get(); + this.handlePGPKey(optContact.get(), rawKey); + } + + void handlePGPKey(Contact contact, byte[] rawKey) { Optional optKey = PGPUtils.readPublicKey(rawKey); if (!optKey.isPresent()) { LOGGER.warning("invalid public PGP key, contact: "+contact); @@ -285,11 +285,12 @@ public void handlePGPKey(JID jid, byte[] rawKey) { // same key return; - if (contact.hasKey()) + if (contact.hasKey()) { // ask before overwriting mViewControl.changed(new ViewEvent.NewKey(contact, key)); - else + } else { this.setKey(contact, key); + } } public void setKey(Contact contact, PGPCoderKey key) { @@ -357,11 +358,21 @@ void maySendKeyRequest(Contact contact) { mClient.sendPublicKeyRequest(contact.getJID()); } + Optional getOrCreate(JID jid, String name) { + Optional optContact = ContactList.getInstance().get(jid); + if (optContact.isPresent()) + return optContact; + + return this.createContact(jid, ""); + } + Optional createContact(JID jid, String name) { return this.createContact(jid, name, XMPPUtils.isKontalkJID(jid)); } - Optional createContact(JID jid, String name, boolean encrypted) { + /* private */ + + private Optional createContact(JID jid, String name, boolean encrypted) { if (!mClient.isConnected()) { // workaround: create only if contact can be added to roster return Optional.empty(); @@ -388,8 +399,6 @@ Optional createContact(JID jid, String name, boolean encrypted) { return Optional.of(newContact); } - /* private */ - private void decryptAndProcess(InMessage message) { if (!message.isEncrypted()) { LOGGER.info("message not encrypted"); @@ -463,13 +472,10 @@ private void processGroupCommand(GroupCommand command, GroupChat chat, Contact s // add contacts if necessary // TODO design problem here: we need at least the public keys, but user // might dont wanna have group members in contact list - ContactList contactList = ContactList.getInstance(); for (JID jid : command.getAdded()) { - if (!contactList.contains(jid)) { - boolean succ = this.createContact(jid, "").isPresent(); - if (!succ) - LOGGER.warning("can't create contact, JID: "+jid); - } + boolean succ = this.getOrCreate(jid, "").isPresent(); + if (!succ) + LOGGER.warning("can't create contact, JID: "+jid); } } diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 2f515190..08efb5a3 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -60,8 +60,6 @@ public enum Error { mClient = client; } - /* from client */ - public void onEntryAdded(JID jid, String name, RosterPacket.ItemType type, @@ -86,7 +84,7 @@ public void onEntryAdded(JID jid, optNewContact.get().setSubScriptionStatus(status); if (status == Contact.Subscription.UNSUBSCRIBED) - mClient.sendPresenceSubscriptionRequest(jid); + mClient.sendPresenceSubscription(jid, Client.PresenceCommand.SUBSCRIBE); } public void onEntryDeleted(JID jid) { @@ -118,6 +116,18 @@ public void onEntryUpdate(JID jid, contact.setName(name); } + public void onSubscriptionRequest(JID jid, byte[] key) { + Optional optContact = mControl.getOrCreate(jid, ""); + if (!optContact.isPresent()) + return; + + // TODO ask user before + mClient.sendPresenceSubscription(jid, Client.PresenceCommand.GRANT); + + if (key.length > 0) + mControl.handlePGPKey(optContact.get(), key); + } + public void onPresenceUpdate(JID jid, Presence.Type type, String status) { if (this.isMe(jid) && !ContactList.getInstance().contains(jid)) // don't wanna see myself From f4b9f4eafb3d57323212de7c5b5f9fd154ed7229 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 28 Nov 2015 18:47:05 +0100 Subject: [PATCH 019/257] client: send blocking command and receive response in one class --- ...seListener.java => BlockSendReceiver.java} | 41 +++++++++++++------ src/main/java/org/kontalk/client/Client.java | 12 +----- 2 files changed, 30 insertions(+), 23 deletions(-) rename src/main/java/org/kontalk/client/{BlockResponseListener.java => BlockSendReceiver.java} (57%) diff --git a/src/main/java/org/kontalk/client/BlockResponseListener.java b/src/main/java/org/kontalk/client/BlockSendReceiver.java similarity index 57% rename from src/main/java/org/kontalk/client/BlockResponseListener.java rename to src/main/java/org/kontalk/client/BlockSendReceiver.java index 0292598a..e664c965 100644 --- a/src/main/java/org/kontalk/client/BlockResponseListener.java +++ b/src/main/java/org/kontalk/client/BlockSendReceiver.java @@ -18,29 +18,30 @@ package org.kontalk.client; +import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.ExceptionCallback; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.kontalk.misc.JID; import org.kontalk.system.Control; /** - * + * Send blocking command and listen to response. * @author Alexander Bikadorov {@literal } */ -final class BlockResponseListener implements StanzaListener { - private static final Logger LOGGER = Logger.getLogger(BlockResponseListener.class.getName()); +final class BlockSendReceiver implements StanzaListener { + private static final Logger LOGGER = Logger.getLogger(BlockSendReceiver.class.getName()); private final Control mControl; - private final XMPPConnection mConn; + private final KonConnection mConn; private final boolean mBlocking; private final JID mJID; - BlockResponseListener(Control control, - XMPPConnection conn, + BlockSendReceiver(Control control, + KonConnection conn, boolean blocking, JID jid){ mControl = control; @@ -49,21 +50,37 @@ final class BlockResponseListener implements StanzaListener { mJID = jid; } + public void sendAndListen() { + LOGGER.info("jid: "+mJID+" blocking="+mBlocking); + + String command = mBlocking ? BlockingCommand.BLOCK : BlockingCommand.UNBLOCK; + BlockingCommand blockingCommand = new BlockingCommand(command, mJID.string()); + + try { + mConn.sendIqWithResponseCallback(blockingCommand, this, new ExceptionCallback() { + @Override + public void processException(Exception exception) { + LOGGER.log(Level.WARNING, "exception response", exception); + } + }); + } catch (SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "not connected", ex); + } + } + @Override public void processPacket(Stanza packet) throws SmackException.NotConnectedException { - LOGGER.info("block response: "+packet); - - mConn.removeSyncStanzaListener(this); + LOGGER.info("response: "+packet); if (!(packet instanceof IQ)) { - LOGGER.warning("block response not an IQ packet"); + LOGGER.warning("response not an IQ packet"); return; } IQ p = (IQ) packet; if (p.getType() != IQ.Type.result) { - LOGGER.warning("ignoring block response with IQ type: "+p.getType()); + LOGGER.warning("ignoring response with IQ type: "+p.getType()); return; } diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 708da97e..7d73bfd9 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -34,7 +34,6 @@ import org.jivesoftware.smack.filter.NotFilter; import org.jivesoftware.smack.filter.OrFilter; import org.jivesoftware.smack.filter.StanzaFilter; -import org.jivesoftware.smack.filter.StanzaIdFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Message; @@ -230,21 +229,12 @@ public void sendBlocklistRequest() { } public void sendBlockingCommand(JID jid, boolean blocking) { - LOGGER.info("jid: "+jid+" blocking="+blocking); - if (mConn == null || !this.isConnected()) { LOGGER.warning("not connected"); return; } - String command = blocking ? BlockingCommand.BLOCK : BlockingCommand.UNBLOCK; - BlockingCommand blockingCommand = new BlockingCommand(command, jid.string()); - - // add response listener - StanzaListener blockResponseListener = new BlockResponseListener(mControl, mConn, blocking, jid); - mConn.addAsyncStanzaListener(blockResponseListener, new StanzaIdFilter(blockingCommand)); - - this.sendPacket(blockingCommand); + new BlockSendReceiver(mControl, mConn, blocking, jid).sendAndListen(); } public void sendInitialPresence() { From 43ef9e75e90ca128d5a9505a9b03b20a74b92a92 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 29 Nov 2015 18:09:55 +0100 Subject: [PATCH 020/257] client: replaced fallback listener with IQ error listener --- src/main/java/org/kontalk/client/Client.java | 23 ++++---------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 7d73bfd9..a6bb65b5 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -31,8 +31,7 @@ import org.jivesoftware.smack.roster.RosterListener; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.filter.NotFilter; -import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.packet.IQ; @@ -40,7 +39,6 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.RosterEntry; -import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.kontalk.system.Config; @@ -128,21 +126,8 @@ public void connect(PersonalKey key) { StanzaFilter presenceFilter = new StanzaTypeFilter(Presence.class); mConn.addAsyncStanzaListener(new PresenceListener(roster, rosterSyncer), presenceFilter); - // fallback listener - mConn.addAsyncStanzaListener(this, - new NotFilter( - new OrFilter( - messageFilter, - vCardFilter, - blockingCommandFilter, - publicKeyFilter, - vCardFilter, - presenceFilter, - // handled by roster listener - new StanzaTypeFilter(RosterPacket.class) - ) - ) - ); + // listen to all IQ errors + mConn.addAsyncStanzaListener(this, IQTypeFilter.ERROR); // continue async List args = new ArrayList<>(0); @@ -294,7 +279,7 @@ synchronized boolean sendPacket(Stanza p) { @Override public void processPacket(Stanza packet) { - LOGGER.config("unhandled: "+packet); + LOGGER.warning("IQ error: "+packet); } public boolean addToRoster(JID jid, String name) { From be65b0d120e1926bcb491f6a87741b552a99d2f8 Mon Sep 17 00:00:00 2001 From: julio amoros Date: Sat, 28 Nov 2015 14:40:45 +0000 Subject: [PATCH 021/257] Translated using Weblate (Catalan) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_ca.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/strings_ca.properties b/src/main/resources/i18n/strings_ca.properties index dc0dcdc0..bae98d6e 100644 --- a/src/main/resources/i18n/strings_ca.properties +++ b/src/main/resources/i18n/strings_ca.properties @@ -425,4 +425,4 @@ s_PC1G=està escrivint… s_KRQ6=Introdueixi la contrasenya… s_2R2P=carregant… s_307W=baixant… -s_4WC0=El certificat del servidor no pot ser validat. +s_4WC0=No s'ha pogut validar el certificat del servidor. From 462c6f16432e501fae11d60e886351e03fff3d35 Mon Sep 17 00:00:00 2001 From: Naofumi Date: Sun, 22 Nov 2015 00:17:05 +0000 Subject: [PATCH 022/257] Translated using Weblate (Japanese) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_ja.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/i18n/strings_ja.properties b/src/main/resources/i18n/strings_ja.properties index 8d5c6f0c..cb9fefd4 100644 --- a/src/main/resources/i18n/strings_ja.properties +++ b/src/main/resources/i18n/strings_ja.properties @@ -283,11 +283,11 @@ s_5X07=このグループを離れました s_FWZZ=グループを変更しました s_OOS3=接続中… s_0WD8=切断中… -s_Z1GT=アーカイブからすべてのキーファイルを読み込みできません。 +s_Z1GT=アーカイブからすべてのキーファイルをロードできません。 s_MMBD=検索… s_ZZWW=他のユーザーにチャットのアクティビティ (入力中、…) を送信 s_PC1G=入力中… s_KRQ6=パスワードを入力… -s_2R2P=読み込み中… +s_2R2P=ロード中… s_307W=ダウンロード中… s_4WC0=サーバー証明書は検証されていません。 From f6fcc0c966d1c79afdf98440d419f54ecedbd49e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 30 Nov 2015 18:09:01 +0100 Subject: [PATCH 023/257] new option for automatically handling presence subscription requests --- src/main/java/org/kontalk/system/Config.java | 2 ++ src/main/java/org/kontalk/view/ConfigurationDialog.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index bd5d7641..97bad67a 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -55,6 +55,7 @@ public final class Config extends PropertiesConfiguration { public static final String NET_SEND_CHAT_STATE = "net.chatstate"; public static final String NET_SEND_ROSTER_NAME = "net.roster_name"; public static final String NET_STATUS_LIST = "net.status_list"; + public static final String NET_AUTO_SUBSCRIPTION = "net.auto_subscription"; public static final String MAIN_CONNECT_STARTUP = "main.connect_startup"; public static final String MAIN_TRAY = "main.tray"; public static final String MAIN_TRAY_CLOSE = "main.tray_close"; @@ -96,6 +97,7 @@ private Config(Path configFile) { map.put(NET_SEND_CHAT_STATE, true); map.put(NET_SEND_ROSTER_NAME, false); map.put(NET_STATUS_LIST, new String[]{DEFAULT_XMPP_STATUS}); + map.put(NET_AUTO_SUBSCRIPTION, false); map.put(MAIN_CONNECT_STARTUP, true); map.put(MAIN_TRAY, true); map.put(MAIN_TRAY_CLOSE, false); diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 38fdef1c..7f884b56 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -297,6 +297,7 @@ private class PrivacyPanel extends WebPanel { private final WebCheckBox mChatStateBox; private final WebCheckBox mRosterNameBox; + private final WebCheckBox mSubscriptionBox; PrivacyPanel() { GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); @@ -305,8 +306,13 @@ private class PrivacyPanel extends WebPanel { groupPanel.add(new WebLabel(Tr.tr("Privacy Settings")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); + mSubscriptionBox = createCheckBox(Tr.tr("Automatically grant authorization"), + Tr.tr("Automatically grant online status authorization requests from other users"), + mConf.getBoolean(Config.NET_AUTO_SUBSCRIPTION)); + groupPanel.add(new GroupPanel(mSubscriptionBox, new WebSeparator())); + mChatStateBox = createCheckBox(Tr.tr("Send chatstate notification"), - Tr.tr("Send chat activity (typing,…) to other user"), + Tr.tr("Send chat activity (typing,…) to other users"), mConf.getBoolean(Config.NET_SEND_CHAT_STATE)); groupPanel.add(new GroupPanel(mChatStateBox, new WebSeparator())); @@ -321,6 +327,7 @@ private class PrivacyPanel extends WebPanel { private void saveConfiguration() { mConf.setProperty(Config.NET_SEND_CHAT_STATE, mChatStateBox.isSelected()); mConf.setProperty(Config.NET_SEND_ROSTER_NAME, mRosterNameBox.isSelected()); + mConf.setProperty(Config.NET_AUTO_SUBSCRIPTION, mSubscriptionBox.isSelected()); } } From 0abb40268aa3e5ac402e33395a9a1a127aef346b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 30 Nov 2015 18:21:51 +0100 Subject: [PATCH 024/257] control: ask view on subscription requests --- src/main/java/org/kontalk/client/Client.java | 7 ++++--- .../org/kontalk/client/PresenceListener.java | 4 ++-- src/main/java/org/kontalk/misc/ViewEvent.java | 9 +++++++++ src/main/java/org/kontalk/system/Control.java | 11 +++++++++++ .../java/org/kontalk/system/RosterHandler.java | 17 +++++++++++------ 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index a6bb65b5..15a543bf 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -59,7 +59,7 @@ public final class Client implements StanzaListener, Runnable { private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); - public enum PresenceCommand {SUBSCRIBE, GRANT}; + public enum PresenceCommand {REQUEST, GRANT, DENY}; private static enum Command {CONNECT, DISCONNECT}; @@ -239,11 +239,12 @@ public void sendInitialPresence() { } public void sendPresenceSubscription(JID jid, PresenceCommand command) { - LOGGER.info("to "+jid+ " command: "+command); + LOGGER.info("to: "+jid+ ", command: "+command); Presence.Type type = null; switch(command) { + case REQUEST: type = Presence.Type.subscribe; break; case GRANT: type = Presence.Type.subscribed; break; - case SUBSCRIBE: type = Presence.Type.subscribe; break; + case DENY: type = Presence.Type.unsubscribed; break; } Presence presence = new Presence(type); presence.setTo(jid.string()); diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index 8d6fa6c3..f7722214 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -93,8 +93,8 @@ public void processPacket(Stanza packet) { mHandler.onSubscriptionRequest(jid, key); return; case unsubscribe: - // TODO - LOGGER.info(("ignoring unsubscribe request, JID: "+jid)); + // nothing to do(?) + LOGGER.info(("ignoring unsubscribe, JID: "+jid)); return; } diff --git a/src/main/java/org/kontalk/misc/ViewEvent.java b/src/main/java/org/kontalk/misc/ViewEvent.java index 27f39004..f79f7f87 100644 --- a/src/main/java/org/kontalk/misc/ViewEvent.java +++ b/src/main/java/org/kontalk/misc/ViewEvent.java @@ -106,4 +106,13 @@ public PresenceError(Contact contact, RosterHandler.Error error) { this.error = error; } } + + /** A contact wants presence subscription (ask whattodo). */ + public static class SubscriptionRequest extends ViewEvent { + public final Contact contact; + + public SubscriptionRequest(Contact contact) { + this.contact = contact; + } + } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 69fa3a2b..b9dfa241 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -370,6 +370,10 @@ Optional createContact(JID jid, String name) { return this.createContact(jid, name, XMPPUtils.isKontalkJID(jid)); } + void sendPresenceSubscription(JID jid, Client.PresenceCommand command) { + mClient.sendPresenceSubscription(jid, command); + } + /* private */ private Optional createContact(JID jid, String name, boolean encrypted) { @@ -633,6 +637,13 @@ public void declineKey(Contact contact) { this.sendContactBlocking(contact, true); } + public void sendSubscriptionResponse(Contact contact, boolean accept) { + Control.this.sendPresenceSubscription(contact.getJID(), + accept ? + Client.PresenceCommand.GRANT : + Client.PresenceCommand.DENY); + } + /* chats */ public Chat getOrCreateSingleChat(Contact contact) { diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 08efb5a3..ee654fa2 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -84,7 +84,7 @@ public void onEntryAdded(JID jid, optNewContact.get().setSubScriptionStatus(status); if (status == Contact.Subscription.UNSUBSCRIBED) - mClient.sendPresenceSubscription(jid, Client.PresenceCommand.SUBSCRIBE); + mControl.sendPresenceSubscription(jid, Client.PresenceCommand.REQUEST); } public void onEntryDeleted(JID jid) { @@ -116,16 +116,21 @@ public void onEntryUpdate(JID jid, contact.setName(name); } - public void onSubscriptionRequest(JID jid, byte[] key) { + public void onSubscriptionRequest(JID jid, byte[] rawKey) { Optional optContact = mControl.getOrCreate(jid, ""); if (!optContact.isPresent()) return; + Contact contact = optContact.get(); - // TODO ask user before - mClient.sendPresenceSubscription(jid, Client.PresenceCommand.GRANT); + if (Config.getInstance().getBoolean(Config.NET_AUTO_SUBSCRIPTION)) { + mControl.sendPresenceSubscription(jid, Client.PresenceCommand.GRANT); + } else { + // ask user + mControl.getViewControl().changed(new ViewEvent.SubscriptionRequest(contact)); + } - if (key.length > 0) - mControl.handlePGPKey(optContact.get(), key); + if (rawKey.length > 0) + mControl.handlePGPKey(contact, rawKey); } public void onPresenceUpdate(JID jid, Presence.Type type, String status) { From a0c802f8183ab830a792abe64ac77abfe9e0e282 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 30 Nov 2015 18:25:14 +0100 Subject: [PATCH 025/257] view: show subscription requests --- src/main/java/org/kontalk/view/Notifier.java | 70 +++++++++++++------- src/main/java/org/kontalk/view/View.java | 64 +++++++++--------- 2 files changed, 80 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/kontalk/view/Notifier.java b/src/main/java/org/kontalk/view/Notifier.java index e0a11da4..9e5b38af 100644 --- a/src/main/java/org/kontalk/view/Notifier.java +++ b/src/main/java/org/kontalk/view/Notifier.java @@ -41,6 +41,7 @@ import org.kontalk.crypto.Coder; import org.kontalk.crypto.PGPUtils; import org.kontalk.misc.KonException; +import org.kontalk.misc.ViewEvent; import org.kontalk.model.Contact; import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; @@ -111,14 +112,7 @@ void showSecurityErrors(KonMessage message) { } void showPresenceError(Contact contact, RosterHandler.Error error) { - WebPanel panel = new GroupPanel(GAP_DEFAULT, false); - panel.setOpaque(false); - - panel.add(new WebLabel(Tr.tr("Contact error")).setBoldFont()); - panel.add(new WebSeparator(true, true)); - - panel.add(new WebLabel(Tr.tr("Contact:")).setBoldFont()); - panel.add(new WebLabel(contactText(contact))); + WebPanel panel = panel(Tr.tr("Contact error"), contact); panel.add(new WebLabel(Tr.tr("Error:")).setBoldFont()); String errorText = Tr.tr(error.toString()); @@ -134,14 +128,7 @@ void showPresenceError(Contact contact, RosterHandler.Error error) { } void confirmNewKey(final Contact contact, final PGPUtils.PGPCoderKey key) { - WebPanel panel = new GroupPanel(GAP_DEFAULT, false); - panel.setOpaque(false); - - panel.add(new WebLabel(Tr.tr("Received new key for contact")).setBoldFont()); - panel.add(new WebSeparator(true, true)); - - panel.add(new WebLabel(Tr.tr("Contact:")).setBoldFont()); - panel.add(new WebLabel(contactText(contact))); + WebPanel panel = panel(Tr.tr("Received new key for contact"), contact); panel.add(new WebLabel(Tr.tr("Key fingerprint:"))); WebTextArea fpArea = Utils.createFingerprintArea(); @@ -174,13 +161,7 @@ public void closed() {} } void confirmContactDeletion(final Contact contact) { - WebPanel panel = new GroupPanel(GAP_DEFAULT, false); - panel.setOpaque(false); - - panel.add(new WebLabel(Tr.tr("Contact was deleted on server")).setBoldFont()); - panel.add(new WebSeparator(true, true)); - - panel.add(new WebLabel(contactText(contact)).setBoldFont()); + WebPanel panel = panel(Tr.tr("Contact was deleted on server"), contact); String expl = Tr.tr("Remove this contact from your contact list?") + "\n" + View.REMOVE_CONTACT_NOTE; @@ -205,6 +186,36 @@ public void closed() {} }); } + void confirmSubscription(ViewEvent.SubscriptionRequest event){ + final Contact contact = event.contact; + + WebPanel panel = panel(Tr.tr("Authorization request"), contact); + + String expl = Tr.tr("When accepting, this contact will be able to see your online status."); + panel.add(textArea(expl)); + + WebNotificationPopup popup = NotificationManager.showNotification(panel, + NotificationOption.accept, NotificationOption.decline, + NotificationOption.cancel); + popup.setClickToClose(false); + popup.addNotificationListener(new NotificationListener() { + @Override + public void optionSelected(NotificationOption option) { + switch (option) { + case accept : + mView.getControl().sendSubscriptionResponse(contact, true); + break; + case decline : + mView.getControl().sendSubscriptionResponse(contact, false); + } + } + @Override + public void accepted() {} + @Override + public void closed() {} + }); + } + // TODO not used private void showNotification() { final WebDialog dialog = new WebDialog(); @@ -262,6 +273,19 @@ public void closed() { NotificationManager.showNotification(dialog, popup); } + private static WebPanel panel(String title, Contact contact) { + WebPanel panel = new GroupPanel(GAP_DEFAULT, false); + panel.setOpaque(false); + + panel.add(new WebLabel(title).setBoldFont()); + panel.add(new WebSeparator(true, true)); + + panel.add(new WebLabel(Tr.tr("Contact:")).setBoldFont()); + panel.add(new WebLabel(contactText(contact))); + + return panel; + } + private static String contactText(Contact contact){ return Utils.name(contact, 20) + " < " + Utils.jid(contact.getJID(), 30)+" >"; } diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 080237fc..19265b5d 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -185,37 +185,39 @@ public void run() { } private void updateOnEDT(Object arg) { - if (arg instanceof ViewEvent.StatusChanged) { - this.statusChanged(); - } else if (arg instanceof ViewEvent.PasswordSet) { - this.showPasswordDialog(false); - } else if (arg instanceof ViewEvent.MissingAccount) { - ViewEvent.MissingAccount missAccount = (ViewEvent.MissingAccount) arg; - this.showImportWizard(missAccount.connect); - } else if (arg instanceof ViewEvent.Exception) { - ViewEvent.Exception exception = (ViewEvent.Exception) arg; - mNotifier.showException(exception.exception); - } else if (arg instanceof ViewEvent.SecurityError) { - ViewEvent.SecurityError error = (ViewEvent.SecurityError) arg; - mNotifier.showSecurityErrors(error.message); - } else if (arg instanceof ViewEvent.NewMessage) { - ViewEvent.NewMessage newMessage = (ViewEvent.NewMessage) arg; - mNotifier.onNewMessage(newMessage.message); - } else if (arg instanceof ViewEvent.NewKey) { - ViewEvent.NewKey newKey = (ViewEvent.NewKey) arg; - if (!newKey.contact.hasKey()) - // TODO webkey, disabling for now - return; - mNotifier.confirmNewKey(newKey.contact, newKey.key); - } else if (arg instanceof ViewEvent.ContactDeleted) { - ViewEvent.ContactDeleted contactDeleted = (ViewEvent.ContactDeleted) arg; - mNotifier.confirmContactDeletion(contactDeleted.contact); - } else if (arg instanceof ViewEvent.PresenceError) { - ViewEvent.PresenceError presenceError = (ViewEvent.PresenceError) arg; - mNotifier.showPresenceError(presenceError.contact, presenceError.error); - } else { - LOGGER.warning("unexpected argument"); - } + if (arg instanceof ViewEvent.StatusChanged) { + this.statusChanged(); + } else if (arg instanceof ViewEvent.PasswordSet) { + this.showPasswordDialog(false); + } else if (arg instanceof ViewEvent.MissingAccount) { + ViewEvent.MissingAccount missAccount = (ViewEvent.MissingAccount) arg; + this.showImportWizard(missAccount.connect); + } else if (arg instanceof ViewEvent.Exception) { + ViewEvent.Exception exception = (ViewEvent.Exception) arg; + mNotifier.showException(exception.exception); + } else if (arg instanceof ViewEvent.SecurityError) { + ViewEvent.SecurityError error = (ViewEvent.SecurityError) arg; + mNotifier.showSecurityErrors(error.message); + } else if (arg instanceof ViewEvent.NewMessage) { + ViewEvent.NewMessage newMessage = (ViewEvent.NewMessage) arg; + mNotifier.onNewMessage(newMessage.message); + } else if (arg instanceof ViewEvent.NewKey) { + ViewEvent.NewKey newKey = (ViewEvent.NewKey) arg; + if (!newKey.contact.hasKey()) + // TODO webkey, disabling for now + return; + mNotifier.confirmNewKey(newKey.contact, newKey.key); + } else if (arg instanceof ViewEvent.ContactDeleted) { + ViewEvent.ContactDeleted contactDeleted = (ViewEvent.ContactDeleted) arg; + mNotifier.confirmContactDeletion(contactDeleted.contact); + } else if (arg instanceof ViewEvent.PresenceError) { + ViewEvent.PresenceError presenceError = (ViewEvent.PresenceError) arg; + mNotifier.showPresenceError(presenceError.contact, presenceError.error); + } else if (arg instanceof ViewEvent.SubscriptionRequest) { + mNotifier.confirmSubscription((ViewEvent.SubscriptionRequest) arg); + } else { + LOGGER.warning("unexpected argument: "+arg); + } } private void statusChanged() { From 8b3380e55c31e66efd0163d7ea5a55efd8788f94 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 30 Nov 2015 18:27:45 +0100 Subject: [PATCH 026/257] user can manually request subscription --- src/main/java/org/kontalk/system/Control.java | 5 ++++ .../java/org/kontalk/view/ContactDetails.java | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index b9dfa241..afb4a85a 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -644,6 +644,11 @@ public void sendSubscriptionResponse(Contact contact, boolean accept) { Client.PresenceCommand.DENY); } + public void sendSubscriptionRequest(Contact contact) { + Control.this.sendPresenceSubscription(contact.getJID(), + Client.PresenceCommand.REQUEST); + } + /* chats */ public Chat getOrCreateSingleChat(Contact contact) { diff --git a/src/main/java/org/kontalk/view/ContactDetails.java b/src/main/java/org/kontalk/view/ContactDetails.java index f0b8c43c..7fbc7f16 100644 --- a/src/main/java/org/kontalk/view/ContactDetails.java +++ b/src/main/java/org/kontalk/view/ContactDetails.java @@ -59,7 +59,8 @@ final class ContactDetails extends WebPanel implements Observer { private final View mView; private final Contact mContact; private final WebTextField mNameField; - private final WebLabel mAuthorization; + private final WebLabel mSubscrStatus; + private final WebButton mSubscrButton; private final WebLabel mKeyStatus; private final WebLabel mFPLabel; private final WebTextArea mFPArea; @@ -117,10 +118,21 @@ protected void onFocusLost() { mainPanel.add(jidField); mainPanel.add(new WebLabel(Tr.tr("Authorization:"))); - mAuthorization = new WebLabel(); - String authText = Tr.tr("Permission to view presence status and public key"); - TooltipManager.addTooltip(mAuthorization, authText); - mainPanel.add(mAuthorization); + mSubscrStatus = new WebLabel(); + String subscrText = Tr.tr("Permission to view presence status and public key"); + TooltipManager.addTooltip(mSubscrStatus, subscrText); + + mSubscrButton = new WebButton(Tr.tr("Request")); + String reqText = Tr.tr("Request status authorization from contact"); + TooltipManager.addTooltip(mSubscrButton, reqText); + mSubscrButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + mView.getControl().sendSubscriptionRequest(mContact); + } + }); + mainPanel.add(new GroupPanel(GroupingType.fillFirst, + View.GAP_DEFAULT, mSubscrStatus, mSubscrButton)); groupPanel.add(mainPanel); @@ -214,7 +226,9 @@ private void updateOnEDT() { case SUBSCRIBED: auth = Tr.tr("Authorized"); break; case UNSUBSCRIBED: auth = Tr.tr("Not authorized"); break; } - mAuthorization.setText(auth); + mSubscrButton.setVisible(subscription != Contact.Subscription.SUBSCRIBED); + mSubscrButton.setEnabled(subscription == Contact.Subscription.UNSUBSCRIBED); + mSubscrStatus.setText(auth); String hasKey = ""; if (mContact.hasKey()) { hasKey += Tr.tr("Available")+""; From f17eff908c0b7bc3716e9c7f0d50b35989d5b97c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 1 Dec 2015 18:51:48 +0100 Subject: [PATCH 027/257] view: cache for contact details + fix update --- .../java/org/kontalk/view/ContactDetails.java | 18 ++++++++++++++---- src/main/java/org/kontalk/view/Content.java | 5 +---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/view/ContactDetails.java b/src/main/java/org/kontalk/view/ContactDetails.java index 7fbc7f16..06d84847 100644 --- a/src/main/java/org/kontalk/view/ContactDetails.java +++ b/src/main/java/org/kontalk/view/ContactDetails.java @@ -39,6 +39,8 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; import java.util.Observable; import java.util.Observer; import javax.swing.Box; @@ -56,6 +58,8 @@ */ final class ContactDetails extends WebPanel implements Observer { + private static final Map CACHE = new HashMap<>(); + private final View mView; private final Contact mContact; private final WebTextField mNameField; @@ -66,7 +70,7 @@ final class ContactDetails extends WebPanel implements Observer { private final WebTextArea mFPArea; private final WebCheckBox mEncryptionBox; - ContactDetails(View view, Contact contact) { + private ContactDetails(View view, Contact contact) { mView = view; mContact = contact; @@ -216,7 +220,7 @@ public void run() { } private void updateOnEDT() { - // may have changed: contact name and/or key + // may have changed: contact name, subscription and/or key mNameField.setText(mContact.getName()); mNameField.setInputPrompt(mContact.getName()); Contact.Subscription subscription = mContact.getSubScription(); @@ -269,7 +273,13 @@ private void saveJID(JID jid) { mView.getControl().changeJID(mContact, jid); } - void onClose() { - this.mContact.deleteObserver(this); + static ContactDetails instance(View view, Contact contact) { + if (!CACHE.containsKey(contact)) { + ContactDetails newContactDetails = new ContactDetails(view, contact); + contact.addObserver(newContactDetails); + CACHE.put(contact, newContactDetails); + } + + return CACHE.get(contact); } } diff --git a/src/main/java/org/kontalk/view/Content.java b/src/main/java/org/kontalk/view/Content.java index fa5573ae..499794e1 100644 --- a/src/main/java/org/kontalk/view/Content.java +++ b/src/main/java/org/kontalk/view/Content.java @@ -56,7 +56,7 @@ void showChat(Chat chat) { } void showContact(Contact contact) { - this.show(new ContactDetails(mView, contact)); + this.show(ContactDetails.instance(mView, contact)); } void showNothing() { @@ -69,9 +69,6 @@ void showNothing() { } private void show(Component comp) { - if (mCurrent instanceof ContactDetails) { - ((ContactDetails) mCurrent).onClose(); - } // Swing... this.removeAll(); this.add(comp, BorderLayout.CENTER); From dfd07af7b08335fa9eef435121528cbe195bbcf4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 2 Dec 2015 19:01:33 +0100 Subject: [PATCH 028/257] view: disable key update before it will fail --- src/main/java/org/kontalk/system/Control.java | 9 +++++---- src/main/java/org/kontalk/view/ContactDetails.java | 11 +++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index afb4a85a..0cb00984 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -347,12 +347,13 @@ boolean sendMessage(OutMessage message) { } void maySendKeyRequest(Contact contact) { - if (!contact.isKontalkUser()) + if (!contact.isKontalkUser()) { + LOGGER.config("not sending, not a kontalk user, contact: "+contact); return; + } - if (contact.getSubScription() == Contact.Subscription.UNSUBSCRIBED || - contact.getSubScription() == Contact.Subscription.PENDING) { - LOGGER.info("no presence subscription, not sending key request, contact: "+contact); + if (contact.getSubScription() != Contact.Subscription.SUBSCRIBED) { + LOGGER.config("not sending, no subscription, contact: "+contact); return; } mClient.sendPublicKeyRequest(contact.getJID()); diff --git a/src/main/java/org/kontalk/view/ContactDetails.java b/src/main/java/org/kontalk/view/ContactDetails.java index 06d84847..3840851c 100644 --- a/src/main/java/org/kontalk/view/ContactDetails.java +++ b/src/main/java/org/kontalk/view/ContactDetails.java @@ -67,6 +67,7 @@ final class ContactDetails extends WebPanel implements Observer { private final WebButton mSubscrButton; private final WebLabel mKeyStatus; private final WebLabel mFPLabel; + private final WebButton mUpdateButton; private final WebTextArea mFPArea; private final WebCheckBox mEncryptionBox; @@ -146,17 +147,17 @@ public void actionPerformed(ActionEvent e) { keyPanel.add(new WebLabel(Tr.tr("Public Key")+":")); mKeyStatus = new WebLabel(); - WebButton updButton = new WebButton(Tr.tr("Update")); + mUpdateButton = new WebButton(Tr.tr("Update")); String updText = Tr.tr("Update key"); - TooltipManager.addTooltip(updButton, updText); - updButton.addActionListener(new ActionListener() { + TooltipManager.addTooltip(mUpdateButton, updText); + mUpdateButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { mView.getControl().requestKey(ContactDetails.this.mContact); } }); keyPanel.add(new GroupPanel(GroupingType.fillFirst, - View.GAP_DEFAULT, mKeyStatus, updButton)); + View.GAP_DEFAULT, mKeyStatus, mUpdateButton)); mFPLabel = new WebLabel(Tr.tr("Fingerprint:")); keyPanel.add(mFPLabel); @@ -248,6 +249,8 @@ private void updateOnEDT() { mFPArea.setVisible(false); } mKeyStatus.setText(hasKey); + mUpdateButton.setEnabled(mContact.isKontalkUser() && + subscription == Contact.Subscription.SUBSCRIBED); } private void saveName(String name) { From 0a51fb216d39a11214c5f52de8a09363ba0fda87 Mon Sep 17 00:00:00 2001 From: Josh Cheung Date: Thu, 3 Dec 2015 11:33:17 +0000 Subject: [PATCH 029/257] Translated using Weblate (Chinese) Currently translated at 100.0% (229 of 229 strings) --- src/main/resources/i18n/strings_zh.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/i18n/strings_zh.properties b/src/main/resources/i18n/strings_zh.properties index e62159a9..a8420cbd 100644 --- a/src/main/resources/i18n/strings_zh.properties +++ b/src/main/resources/i18n/strings_zh.properties @@ -227,3 +227,4 @@ s_PC1G=正在撰写…… s_KRQ6=输入密码…… s_2R2P=正在载入…… s_307W=正在下载…… +s_4WC0=服务器证书无法被验证。 From b0b13f656106e434c992ee8c1aacc1144c55c6ca Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 4 Dec 2015 20:31:42 +0100 Subject: [PATCH 030/257] view: new avatar loader (unused) --- src/main/java/org/kontalk/model/Contact.java | 2 +- .../java/org/kontalk/view/AvatarLoader.java | 152 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/kontalk/view/AvatarLoader.java diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index fb9ddb4d..f450c4a7 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -144,7 +144,7 @@ void setJID(JID jid) { this.changed(mJID); } - int getID() { + public int getID() { return mID; } diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java new file mode 100644 index 00000000..7c23918d --- /dev/null +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -0,0 +1,152 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.view; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.swing.ImageIcon; +import org.kontalk.model.Chat; +import org.kontalk.model.Contact; + +/** + * Static functions for loading avatar pictures. + * @author Alexander Bikadorov {@literal } + */ +final class AvatarLoader { + + private static final int IMG_SIZE = 40; + private static final Color LETTER_COLOR = new Color(255, 255, 255); + // TODO i18n? + private static final String FALLBACK_LETTER = "?"; + private static final Color FALLBACK_COLOR = new Color(220, 220, 220); + + private static final Map CACHE = new HashMap<>(); + + AvatarLoader() {}; + + static ImageIcon load(Chat chat) { + return load(new Item(chat)); + } + + static ImageIcon load(Contact contact) { + return load(new Item(contact)); + } + + private static ImageIcon load(Item item) { + if (!CACHE.containsKey(item)) { + // TODO + CACHE.put(item, fallback(item)); + } + return CACHE.get(item); + } + + private static ImageIcon fallback(Item item) { + BufferedImage img = new BufferedImage(IMG_SIZE, IMG_SIZE, BufferedImage.TYPE_INT_RGB); + + // color + Color color; + if (!item.label.isEmpty()) { + int hue = Math.abs(item.colorCode) % 360; + color = Color.getHSBColor(hue / 360.0f, 1, 1); + } else { + color = FALLBACK_COLOR; + } + + Graphics2D graphics = img.createGraphics(); + graphics.setColor(color); + graphics.fillRect(0, 0, IMG_SIZE, IMG_SIZE); + + // letter + String name = item.label; + String letter = name.length() > 1 ? + name.substring(0, 1).toUpperCase() : + FALLBACK_LETTER; + + graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, IMG_SIZE)); + graphics.setColor(LETTER_COLOR); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + FontMetrics fm = graphics.getFontMetrics(); + int w = fm.stringWidth(letter); + int h = fm.getHeight(); + int d = fm.getDescent(); + graphics.drawString(letter, + (IMG_SIZE / 2.0f) - (w / 2.0f), + // adjust to font baseline + (IMG_SIZE / 2.0f) + (h / 2.0f) - d); + + return new ImageIcon(img); + } + + private static class Item { + final String label; + final int colorCode; + + Item(Contact contact) { + label = contact.getName(); + colorCode = hash(contact.getID()); + } + + Item(Chat chat) { + if (chat.isGroupChat()) { + label = chat.getSubject(); + } else { + Contact[] contacts = chat.getValidContacts(); + label = contacts.length > 0 ? contacts[0].getName() : ""; + } + colorCode = hash(chat.getID()); + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Item)) + return false; + + Item oItem = (Item) o; + return label.equals(oItem.label) && colorCode == oItem.colorCode; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 37 * hash + Objects.hashCode(this.label); + hash = 37 * hash + this.colorCode; + return hash; + } + } + + // uniform hash + // Source: https://stackoverflow.com/a/12996028 + private static int hash(int x) { + x = ((x >> 16) ^ x) * 0x45d9f3b; + x = ((x >> 16) ^ x) * 0x45d9f3b; + x = ((x >> 16) ^ x); + return x; + } +} From 3209b57cacdec0246641c8f92a2988697e463252 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 17:52:08 +0100 Subject: [PATCH 031/257] account class moved to model --- src/main/java/org/kontalk/Kontalk.java | 2 +- src/main/java/org/kontalk/crypto/Coder.java | 2 +- src/main/java/org/kontalk/{system => model}/Account.java | 9 +++++---- src/main/java/org/kontalk/system/AccountImporter.java | 1 + src/main/java/org/kontalk/system/AttachmentManager.java | 1 + src/main/java/org/kontalk/system/Control.java | 3 ++- src/main/java/org/kontalk/view/ConfigurationDialog.java | 2 +- src/main/java/org/kontalk/view/ImportDialog.java | 2 +- 8 files changed, 13 insertions(+), 9 deletions(-) rename src/main/java/org/kontalk/{system => model}/Account.java (96%) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 16529677..2ed86321 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -36,7 +36,7 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.model.ChatList; import org.kontalk.model.ContactList; -import org.kontalk.system.Account; +import org.kontalk.model.Account; import org.kontalk.system.Config; import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index b0ed6548..1b5cd69d 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -29,7 +29,7 @@ import org.kontalk.model.OutMessage; import org.kontalk.model.DecryptMessage; import org.kontalk.model.InMessage; -import org.kontalk.system.Account; +import org.kontalk.model.Account; /** * Static methods for decryption and encryption of a message. diff --git a/src/main/java/org/kontalk/system/Account.java b/src/main/java/org/kontalk/model/Account.java similarity index 96% rename from src/main/java/org/kontalk/system/Account.java rename to src/main/java/org/kontalk/model/Account.java index 638c7d7f..1d29dd2b 100644 --- a/src/main/java/org/kontalk/system/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.system; +package org.kontalk.model; import java.io.File; import java.io.FileInputStream; @@ -39,6 +39,7 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; +import org.kontalk.system.Config; /** * The user account. There can only be one. @@ -66,7 +67,7 @@ public Optional getPersonalKey() { return Optional.ofNullable(mKey); } - PersonalKey load(char[] password) throws KonException { + public PersonalKey load(char[] password) throws KonException { // read key files byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); byte[] bridgeCertData = this.readFile(BRIDGE_CERT_FILENAME, false); @@ -83,7 +84,7 @@ PersonalKey load(char[] password) throws KonException { return mKey; } - void setAccount(byte[] privateKeyData, char[] password) throws KonException { + public void setAccount(byte[] privateKeyData, char[] password) throws KonException { // try to load key PersonalKey key; byte[] encodedPrivateKey; @@ -151,7 +152,7 @@ private void writePrivateKey(byte[] privateKeyData, mConf.setProperty(Config.ACC_PASS, savedPass); } - boolean accountIsPresent() { + public boolean isPresent() { return this.fileExists(PRIVATE_KEY_FILENAME) && this.fileExists(BRIDGE_CERT_FILENAME); } diff --git a/src/main/java/org/kontalk/system/AccountImporter.java b/src/main/java/org/kontalk/system/AccountImporter.java index dea33d12..6eb4b02c 100644 --- a/src/main/java/org/kontalk/system/AccountImporter.java +++ b/src/main/java/org/kontalk/system/AccountImporter.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.model.Account; import java.io.IOException; import java.util.Observable; import java.util.Observer; diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 062d4dec..736f5163 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.model.Account; import java.awt.Dimension; import java.awt.Image; import java.awt.image.BufferedImage; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 0cb00984..aee16191 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.model.Account; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -527,7 +528,7 @@ public void launch() { new Thread(mClient).start(); boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); - if (!Account.getInstance().accountIsPresent()) { + if (!Account.getInstance().isPresent()) { LOGGER.info("no account found, asking for import..."); this.changed(new ViewEvent.MissingAccount(connect)); return; diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 7f884b56..3ce627d0 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -51,7 +51,7 @@ import org.kontalk.system.Config; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; -import org.kontalk.system.Account; +import org.kontalk.model.Account; import org.kontalk.util.Tr; /** diff --git a/src/main/java/org/kontalk/view/ImportDialog.java b/src/main/java/org/kontalk/view/ImportDialog.java index cb71e318..eb935194 100644 --- a/src/main/java/org/kontalk/view/ImportDialog.java +++ b/src/main/java/org/kontalk/view/ImportDialog.java @@ -49,7 +49,7 @@ import javax.swing.filechooser.FileNameExtensionFilter; import org.kontalk.misc.KonException; import org.kontalk.system.AccountImporter; -import org.kontalk.system.Account; +import org.kontalk.model.Account; import org.kontalk.util.Tr; /** From 5407286694a251e4d678790f834511b1f91ea350 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 18:09:02 +0100 Subject: [PATCH 032/257] client: setting entity capabilities persistent cache --- src/main/java/org/kontalk/client/Client.java | 19 +++++++++++++++++++ src/main/java/org/kontalk/system/Control.java | 1 + 2 files changed, 20 insertions(+) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 15a543bf..a34edbc2 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -18,7 +18,9 @@ package org.kontalk.client; +import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -39,6 +41,8 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.RosterEntry; +import org.jivesoftware.smackx.caps.EntityCapsManager; +import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.kontalk.system.Config; @@ -57,6 +61,7 @@ public final class Client implements StanzaListener, Runnable { private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + private static final String CAPS_CACHE_DIR = "caps_cache"; private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); public enum PresenceCommand {REQUEST, GRANT, DENY}; @@ -79,6 +84,20 @@ public Client(Control control) { //SmackConfiguration.DEBUG = true; } + public static void setCapsCache(Path appDir) { + File cacheDir = appDir.resolve(CAPS_CACHE_DIR).toFile(); + if (cacheDir.mkdir()) + LOGGER.info("created caps cache directory"); + + if (!cacheDir.isDirectory()) { + LOGGER.warning("invalid cache directory: "+cacheDir); + return; + } + + EntityCapsManager.setPersistentCache( + new SimpleDirectoryPersistentCache(cacheDir)); + } + public void connect(PersonalKey key) { this.disconnect(); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index aee16191..aa911b1f 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -491,6 +491,7 @@ private void processGroupCommand(GroupCommand command, GroupChat chat, Contact s /* static */ public static ViewControl create(Path appDir) { + Client.setCapsCache(appDir); return new Control(appDir).mViewControl; } From 3b27969719adecf4da62548905392f1812fdaea9 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 18:15:38 +0100 Subject: [PATCH 033/257] client: lots of stuff for the future --- src/main/java/org/kontalk/client/Client.java | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index a34edbc2..21e449dd 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -180,6 +180,64 @@ private void connectAsync() { mConn.addStanzaAcknowledgedListener(new AcknowledgedListener(mControl)); + // (server) service discovery, XEP-0030 + // NOTE: smack automatically creates instances of SDM and CapsM and connects them + //ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn); +// try { +// // blocking +// // NOTE: null parameter does not work +// DiscoverInfo i = discoManager.discoverInfo(mConn.getServiceName()); +// for (DiscoverInfo.Feature f: i.getFeatures()) { +// System.out.println("server feature: "+f.getVar()); +// } +// } catch (SmackException.NoResponseException | +// XMPPException.XMPPErrorException | +// SmackException.NotConnectedException ex) { +// Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex); +// } + + // Caps, XEP-0115 + // NOTE: caps manager is automatically used by Smack + //EntityCapsManager capsManager = EntityCapsManager.getInstanceFor(mConn); + + // PEP, XEP-0163 + // NOTE: Smack's implementation is not usable, use PubSub instead +// PEPManager m = new PEPManager(mConn); +// m.addPEPListener(new PEPListener() { +// @Override +// public void eventReceived(String from, PEPEvent event) { +// LOGGER.info("from: "+from+" event: "+event); +// } +// }); + + // PubSub, XEP-0060 + // NOTE: pubsub is currently unsupported by beta.kontalk.net +// PubSubManager pubSubManager = new PubSubManager(mConn, mConn.getServiceName()); +// try { +// DiscoverInfo i = pubSubManager.getSupportedFeatures(); +// // same as server service discovery features!? +// for (DiscoverInfo.Feature f: i.getFeatures()) { +// System.out.println("feature: "+f.getVar()); +// } +// } catch (SmackException.NoResponseException | +// XMPPException.XMPPErrorException | +// SmackException.NotConnectedException ex) { +// Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex); +// } + // here be exceptions +// try { +// for (Affiliation a: pubSubManager.getAffiliations()) { +// System.out.println("aff: "+a.toXML()); +// } +// for (Subscription s: pubSubManager.getSubscriptions()) { +// System.out.println("subs: "+s.toXML()); +// } +// } catch (SmackException.NoResponseException | +// XMPPException.XMPPErrorException | +// SmackException.NotConnectedException ex) { +// Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex); +// } + this.sendInitialPresence(); this.sendBlocklistRequest(); From 3d90ffd8f6d472e65f47f347c5ad6b9cb1285fc5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 18:37:17 +0100 Subject: [PATCH 034/257] view: avatar loader improved (still unused) --- .../java/org/kontalk/view/AvatarLoader.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 7c23918d..511b04fc 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -58,18 +58,22 @@ static ImageIcon load(Contact contact) { private static ImageIcon load(Item item) { if (!CACHE.containsKey(item)) { // TODO - CACHE.put(item, fallback(item)); + CACHE.put(item, new ImageIcon(fallback(item.label, item.colorCode, IMG_SIZE))); } return CACHE.get(item); } - private static ImageIcon fallback(Item item) { - BufferedImage img = new BufferedImage(IMG_SIZE, IMG_SIZE, BufferedImage.TYPE_INT_RGB); + static BufferedImage createFallback(int size) { + return fallback("", 0, size); + } + + private static BufferedImage fallback(String text, int colorCode, int size) { + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); // color Color color; - if (!item.label.isEmpty()) { - int hue = Math.abs(item.colorCode) % 360; + if (!text.isEmpty()) { + int hue = Math.abs(colorCode) % 360; color = Color.getHSBColor(hue / 360.0f, 1, 1); } else { color = FALLBACK_COLOR; @@ -77,15 +81,14 @@ private static ImageIcon fallback(Item item) { Graphics2D graphics = img.createGraphics(); graphics.setColor(color); - graphics.fillRect(0, 0, IMG_SIZE, IMG_SIZE); + graphics.fillRect(0, 0, size, size); // letter - String name = item.label; - String letter = name.length() > 1 ? - name.substring(0, 1).toUpperCase() : + String letter = text.length() > 1 ? + text.substring(0, 1).toUpperCase() : FALLBACK_LETTER; - graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, IMG_SIZE)); + graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, size)); graphics.setColor(LETTER_COLOR); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -94,11 +97,11 @@ private static ImageIcon fallback(Item item) { int h = fm.getHeight(); int d = fm.getDescent(); graphics.drawString(letter, - (IMG_SIZE / 2.0f) - (w / 2.0f), + (size / 2.0f) - (w / 2.0f), // adjust to font baseline - (IMG_SIZE / 2.0f) + (h / 2.0f) - d); + (size / 2.0f) + (h / 2.0f) - d); - return new ImageIcon(img); + return img; } private static class Item { From 77c86925867084a46b42584f17a85d77b94484dc Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Dec 2015 17:44:20 +0100 Subject: [PATCH 035/257] util: more functions for image processing --- .../org/kontalk/system/AttachmentManager.java | 2 +- .../java/org/kontalk/util/MediaUtils.java | 39 ++++++++++++++++++- src/main/java/org/kontalk/view/ChatView.java | 2 +- .../java/org/kontalk/view/ImageLoader.java | 2 +- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 736f5163..39211a31 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -266,7 +266,7 @@ boolean createImagePreview(KonMessage message) { && image.getHeight() <= THUMBNAIL_DIM.height) return false; - Image thumb = MediaUtils.scale(image, + Image thumb = MediaUtils.scaleAsync(image, THUMBNAIL_DIM.width , THUMBNAIL_DIM.height, false); diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 6ae643bc..b2387d21 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -21,9 +21,12 @@ import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; @@ -31,6 +34,7 @@ import org.apache.tika.mime.MimeType; import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypes; +import org.kontalk.misc.Callback; /** * @@ -89,8 +93,16 @@ public static BufferedImage readImage(String path) { return new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB); } - public static byte[] imageToByteArray(Image image, String format) { + public static Optional readImage(byte[] imgData) { + try { + return Optional.ofNullable(ImageIO.read(new ByteArrayInputStream(imgData))); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't read image data", ex); + } + return Optional.empty(); + } + public static byte[] imageToByteArray(Image image, String format) { BufferedImage bufImage = new BufferedImage( image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB); @@ -118,7 +130,30 @@ public static byte[] imageToByteArray(Image image, String format) { * @param max specifies if image is scaled to maximum or minimum of width/height * @return the scaled image, loaded async */ - public static Image scale(Image image, int width, int height, boolean max) { + public static void scale(Image image, int width, int height, boolean max, + final Callback.Handler handler) { + Image img = scaleAsync(image, width, height, max); + + ImageObserver observer = new ImageObserver() { + @Override + public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { + // ignore if image is not completely loaded + if ((infoflags & ImageObserver.ALLBITS) == 0) { + return true; + } + + handler.handle(new Callback<>(img)); + return false; + } + }; + + if (img.getWidth(observer) == -1) + return; + + handler.handle(new Callback<>(img)); + } + + public static Image scaleAsync(Image image, int width, int height, boolean max) { int iw = image.getWidth(null); int ih = image.getHeight(null); if (iw == -1) { diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 1e3be2d7..b31acc64 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -571,7 +571,7 @@ private boolean scaleOrigin() { this.updateCachedBG(null); return true; } - Image scaledImage = MediaUtils.scale(mOrigin, + Image scaledImage = MediaUtils.scaleAsync(mOrigin, mParent.getWidth(), mParent.getHeight(), true); diff --git a/src/main/java/org/kontalk/view/ImageLoader.java b/src/main/java/org/kontalk/view/ImageLoader.java index fa2f1363..da4ef8b6 100644 --- a/src/main/java/org/kontalk/view/ImageLoader.java +++ b/src/main/java/org/kontalk/view/ImageLoader.java @@ -56,7 +56,7 @@ private static final class AsyncLoader implements Runnable, ImageObserver { @Override public void run() { BufferedImage image = MediaUtils.readImage(path); - Image scaledImage = MediaUtils.scale(image, + Image scaledImage = MediaUtils.scaleAsync(image, AttachmentManager.THUMBNAIL_DIM.width, AttachmentManager.THUMBNAIL_DIM.height, false); From 903f9897c4406d4db6065c39d88b8b5f61bbf1ae Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Dec 2015 17:53:35 +0100 Subject: [PATCH 036/257] basic support for contact user avatars (not activated) --- .../kontalk/client/AvatarSendReceiver.java | 181 ++++++++++++++++++ src/main/java/org/kontalk/client/Client.java | 25 ++- .../org/kontalk/client/KonConnection.java | 30 +++ .../kontalk/client/KonMessageListener.java | 54 +++++- src/main/java/org/kontalk/model/Contact.java | 41 ++++ .../org/kontalk/system/AvatarHandler.java | 109 +++++++++++ src/main/java/org/kontalk/system/Control.java | 10 +- 7 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/kontalk/client/AvatarSendReceiver.java create mode 100644 src/main/java/org/kontalk/system/AvatarHandler.java diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java new file mode 100644 index 00000000..50b1eeab --- /dev/null +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -0,0 +1,181 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smackx.pubsub.Item; +import org.jivesoftware.smackx.pubsub.ItemsExtension; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubElementType; +import org.jivesoftware.smackx.pubsub.packet.PubSub; +import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; +import org.kontalk.misc.JID; +import org.kontalk.system.AvatarHandler; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +final class AvatarSendReceiver { + private static final Logger LOGGER = Logger.getLogger(AvatarSendReceiver.class.getName()); + + static final String NOTIFY_FEATURE = "urn:xmpp:avatar:metadata+notify"; + static final String METADATA_NODE = "urn:xmpp:avatar:metadata"; + + private static final String DATA_NODE = "urn:xmpp:avatar:data"; + + static { + ProviderManager.addExtensionProvider( + AvatarMetadataExtension.ELEMENT_NAME, + AvatarMetadataExtension.NAMESPACE, + new AvatarMetadataExtension.Provider()); + + ProviderManager.addExtensionProvider( + AvatarDataExtension.ELEMENT_NAME, + AvatarDataExtension.NAMESPACE, + new AvatarDataExtension.Provider()); + + } + + private final KonConnection mConn; + private final AvatarHandler mHandler; + + AvatarSendReceiver(KonConnection conn, AvatarHandler handler) { + mConn = conn; + mHandler = handler; + } + + // TODO beta.kontalk.net does not support this + void publish(String id, byte[] data) { + // TODO + } + + void processMetadataEvent(JID jid, ItemsExtension itemsExt) { + List items = itemsExt.getItems(); + if (items.isEmpty()) { + LOGGER.warning("no items in items event"); + return; + } + + // there should be only one item + ExtensionElement e = items.get(0); + if (!(e instanceof PayloadItem)) { + LOGGER.warning("element not a payloaditem"); + return; + } + + PayloadItem item = (PayloadItem) e; + ExtensionElement metadataExt = item.getPayload(); + if (!(metadataExt instanceof AvatarMetadataExtension)) { + LOGGER.warning("payload not avatar metadata"); + return; + } + AvatarMetadataExtension metadata = (AvatarMetadataExtension) metadataExt; + List infos = metadata.getInfos(); + if (infos.isEmpty()) { + // this means the contact disabled avatar publishing + mHandler.onNotify(jid, ""); + return; + } + // assuming infos are always in the same order + for (AvatarMetadataExtension.Info info : infos) { + if (AvatarHandler.SUPPORTED_TYPES.contains(info.getType())) { + mHandler.onNotify(jid, info.getId()); + break; + } else { + LOGGER.info("image type not supported: "+info.getType()); + } + } + } + + void requestAndListen(final JID jid, final String id) { + // I really dont get how to use this + //PubSubManager manager = new PubSubManager(conn); + + PubSub request = new PubSub(jid.toBare().string(), + IQ.Type.get, + PubSubNamespace.BASIC); + + request.addExtension( + new ItemsExtension( + ItemsExtension.ItemsElementType.items, + DATA_NODE, + Arrays.asList(new Item(id)))); + + // handle response + StanzaListener callback = new StanzaListener() { + @Override + public void processPacket(Stanza packet) + throws SmackException.NotConnectedException { + + if (!(packet instanceof PubSub)) { + LOGGER.warning("response not a pubsub packet"); + return; + } + PubSub pubSub = (PubSub) packet; + + ExtensionElement itemsExt = pubSub.getExtension(PubSubElementType.ITEMS); + if (!(itemsExt instanceof ItemsExtension)) { + LOGGER.warning("no items extension in response"); + return; + } + + ItemsExtension items = (ItemsExtension) itemsExt; + List itemsList = items.getItems(); + if (itemsList.isEmpty()) { + LOGGER.warning("no items in itemlist"); + } + + // there should be only one item + ExtensionElement e = itemsList.get(0); + if (!(e instanceof PayloadItem)) { + LOGGER.warning("element not a payloaditem"); + return; + } + + PayloadItem item = (PayloadItem) e; + ExtensionElement dataExt = item.getPayload(); + if (!(dataExt instanceof AvatarDataExtension)) { + LOGGER.warning("payload not avatar data"); + return; + } + + AvatarDataExtension avatarExt = (AvatarDataExtension) dataExt; + + byte[] avatarData = avatarExt.getData(); + if (avatarData.length == 0) { + LOGGER.warning("no avatar data in packet"); + return; + } + + mHandler.onData(jid, id, avatarData); + } + }; + + mConn.sendWithCallback(request, callback); + } +} diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 21e449dd..ced7fec4 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -71,6 +71,7 @@ private static enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; + private AvatarSendReceiver mAvatarSendReceiver; private KonConnection mConn = null; @@ -118,6 +119,8 @@ public void connect(PersonalKey key) { key.getBridgeCertificate(), validateCertificate); + mAvatarSendReceiver = new AvatarSendReceiver(mConn, mControl.getAvatarHandler()); + // connection listener mConn.addConnectionListener(new KonConnectionListener(mControl)); @@ -131,7 +134,9 @@ public void connect(PersonalKey key) { roster.addRosterListener(rl); StanzaFilter messageFilter = new StanzaTypeFilter(Message.class); - mConn.addAsyncStanzaListener(new KonMessageListener(this, mControl), messageFilter); + mConn.addAsyncStanzaListener( + new KonMessageListener(this, mControl, mAvatarSendReceiver), + messageFilter); StanzaFilter vCardFilter = new StanzaTypeFilter(VCard4.class); mConn.addAsyncStanzaListener(new VCardListener(mControl), vCardFilter); @@ -345,14 +350,12 @@ synchronized boolean sendPackets(Stanza[] stanzas) { } synchronized boolean sendPacket(Stanza p) { - try { - mConn.sendStanza(p); - } catch (SmackException.NotConnectedException ex) { - LOGGER.info("can't send packet, not connected."); + if (mConn == null) { + LOGGER.warning("not connected"); return false; } - LOGGER.config("packet: "+p); - return true; + + return mConn.send(p); } @Override @@ -425,6 +428,14 @@ public boolean updateRosterEntry(JID jid, String newName) { return true; } + public void requestAvatar(JID jid, String id) { + if (mAvatarSendReceiver == null) { + LOGGER.warning("no avatar sender"); + return; + } + mAvatarSendReceiver.requestAndListen(jid, id); + } + @Override public void run() { while (true) { diff --git a/src/main/java/org/kontalk/client/KonConnection.java b/src/main/java/org/kontalk/client/KonConnection.java index f9c96b07..f95c5bd4 100644 --- a/src/main/java/org/kontalk/client/KonConnection.java +++ b/src/main/java/org/kontalk/client/KonConnection.java @@ -36,7 +36,12 @@ import javax.security.auth.callback.UnsupportedCallbackException; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; +import org.jivesoftware.smack.ExceptionCallback; import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration.Builder; @@ -148,4 +153,29 @@ String getServer() { boolean hasLoginCredentials() { return mHasLoginCredentials; } + + boolean send(Stanza p) { + try { + super.sendStanza(p); + } catch (SmackException.NotConnectedException ex) { + LOGGER.info("can't send packet, not connected."); + return false; + } + LOGGER.config("packet: "+p); + return true; + } + + void sendWithCallback(IQ packet, StanzaListener callback) { + LOGGER.config("packet: "+packet); + try { + super.sendIqWithResponseCallback(packet, callback, new ExceptionCallback() { + @Override + public void processException(Exception ex) { + LOGGER.log(Level.WARNING, "exception response", ex); + } + }); + } catch (SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "not connected", ex); + } + } } diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 61a2542a..481f59dd 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -35,6 +35,11 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.delay.packet.DelayInformation; +import org.jivesoftware.smackx.pubsub.EventElement; +import org.jivesoftware.smackx.pubsub.EventElementType; +import org.jivesoftware.smackx.pubsub.ItemsExtension; +import org.jivesoftware.smackx.pubsub.NodeExtension; +import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.misc.JID; @@ -57,24 +62,28 @@ final public class KonMessageListener implements StanzaListener { private final Client mClient; private final Control mControl; + private final AvatarSendReceiver mAvatarHandler; - KonMessageListener(Client client, Control control) { - mClient = client; - mControl = control; - + static { ProviderManager.addExtensionProvider(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE, new E2EEncryption.Provider()); ProviderManager.addExtensionProvider(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE, new OutOfBandData.Provider()); ProviderManager.addExtensionProvider(BitsOfBinary.ELEMENT_NAME, BitsOfBinary.NAMESPACE, new BitsOfBinary.Provider()); ProviderManager.addExtensionProvider(GroupExtension.ELEMENT_NAME, GroupExtension.NAMESPACE, new GroupExtension.Provider()); } + KonMessageListener(Client client, Control control, AvatarSendReceiver avatarHandler) { + mClient = client; + mControl = control; + mAvatarHandler = avatarHandler; + } + @Override public void processPacket(Stanza packet) { Message m = (Message) packet; + Message.Type type = m.getType(); // check for delivery receipt (XEP-0184) - if (m.getType() == Message.Type.normal || - m.getType() == Message.Type.chat) { + if (type == Message.Type.normal || type == Message.Type.chat) { DeliveryReceipt receipt = DeliveryReceipt.from(m); if (receipt != null) { // HOORAY! our message was received @@ -83,13 +92,13 @@ public void processPacket(Stanza packet) { } } - if (m.getType() == Message.Type.chat) { + if (type == Message.Type.chat) { // somebody has news for us this.processChatMessage(m); return; } - if (m.getType() == Message.Type.error) { + if (type == Message.Type.error) { LOGGER.warning("got error message: "+m); XMPPError error = m.getError(); @@ -102,6 +111,11 @@ public void processPacket(Stanza packet) { return; } + if (type == Message.Type.headline) { + this.processHeadlineMessage(m); + return; + } + LOGGER.warning("unhandled message: "+m); } @@ -183,6 +197,30 @@ private void processChatMessage(Message m) { } } + private void processHeadlineMessage(Message m) { + LOGGER.config("message: "+m); + + // this should be a pubsub event + PubSubNamespace ns = PubSubNamespace.EVENT; + ExtensionElement eventExt = m.getExtension(ns.getFragment(), ns.getXmlns()); + if (eventExt instanceof EventElement){ + EventElement event = (EventElement) eventExt; + + if (event.getEventType() == EventElementType.items) { + NodeExtension extension = event.getEvent(); + if (extension instanceof ItemsExtension) { + ItemsExtension items = (ItemsExtension) extension; + if (items.getNode().equals(AvatarSendReceiver.METADATA_NODE)) { + mAvatarHandler.processMetadataEvent(JID.full(m.getFrom()), items); + return; + } + } + } + } + + LOGGER.warning("unhandled"); + } + public static MessageContent parseMessageContent(Message m) { // default body String plainText = StringUtils.defaultString(m.getBody()); diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index f450c4a7..1f715c12 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -26,6 +26,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Observable; import java.util.Optional; import java.util.logging.Level; @@ -88,6 +89,7 @@ public static enum Subscription { private boolean mBlocked = false; private Subscription mSubStatus = Subscription.UNKNOWN; //private ItemType mType; + private Avatar mAvatar = null; // used for creating new contacts (eg from roster) Contact(JID jid, String name) { @@ -256,6 +258,17 @@ public void setSubScriptionStatus(Subscription status) { this.changed(mSubStatus); } + public Optional getAvatar() { + return Optional.ofNullable(mAvatar); + } + + public void setAvatar(Avatar avatar) { + mAvatar = avatar; + // TODO + //this.save(); + this.changed(mAvatar); + } + public boolean isMe() { return mJID.isMe(); } @@ -276,6 +289,7 @@ void setDeleted() { mEncrypted = false; mKey = ""; mFingerprint = ""; + mAvatar = null; this.save(); this.changed(null); @@ -324,4 +338,31 @@ static Contact load(ResultSet rs) throws SQLException { String fp = Database.getString(rs, Contact.COL_KEY_FP); return new Contact(id, jid, name, status, lastSeen, encr, key, fp); } + + public static class Avatar { + public final String id; + public final byte[] data; + + public Avatar(String id, byte[] data) { + this.id = id; + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof Avatar)) return false; + + Avatar oAvatar = (Avatar) o; + return id.equals(oAvatar.id); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 59 * hash + Objects.hashCode(this.id); + return hash; + } + } } diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java new file mode 100644 index 00000000..5b117317 --- /dev/null +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -0,0 +1,109 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.system; + +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; +import javax.imageio.ImageIO; +import org.apache.commons.codec.digest.DigestUtils; +import org.kontalk.client.Client; +import org.kontalk.misc.Callback; +import org.kontalk.misc.JID; +import org.kontalk.model.Contact; +import org.kontalk.model.Contact.Avatar; +import org.kontalk.model.ContactList; +import org.kontalk.util.MediaUtils; + +/** + * Process incoming avatar events + * @author Alexander Bikadorov {@literal } + */ +public final class AvatarHandler { + private static final Logger LOGGER = Logger.getLogger(AvatarHandler.class.getName()); + + public static final List SUPPORTED_TYPES = Arrays.asList(ImageIO.getReaderMIMETypes()); + + private static final int MAX_SIZE = 40; + private static final String STORE_FORMAT = "jpg"; + + private final Client mClient; + + AvatarHandler(Client client) { + mClient = client; + } + + public void onNotify(JID jid, String id) { + Optional optContact = ContactList.getInstance().get(jid); + if (!optContact.isPresent()) { + LOGGER.warning("can't find contact with jid:" + jid); + return; + } + Contact contact = optContact.get(); + + if (id.isEmpty()) { + // contact disabled avatar publishing + // TODO + } + + Optional optAvatar = contact.getAvatar(); + if (optAvatar.isPresent() && + id.equals(DigestUtils.sha1Hex(optAvatar.get().id))) + // avatar is not new + return; + + mClient.requestAvatar(jid, id); + } + + public void onData(JID jid, final String id, byte[] avatarData) { + LOGGER.info("new avatar, jid: "+jid+" id: "+id); + + final Optional optContact = ContactList.getInstance().get(jid); + if (!optContact.isPresent()) { + LOGGER.warning("can't find contact with jid:" + jid); + return; + } + + if (!id.equals(DigestUtils.sha1Hex(avatarData))) { + LOGGER.warning("this is not what we wanted"); + return; + } + + Optional optImg = MediaUtils.readImage(avatarData); + if (!optImg.isPresent()) + return; + + MediaUtils.scale(optImg.get(), MAX_SIZE, MAX_SIZE, true, + new Callback.Handler(){ + @Override + public void handle(Callback callback) { + byte[] data = MediaUtils.imageToByteArray(callback.value, STORE_FORMAT); + if (data.length == 0) + return; + + Contact contact = optContact.get(); + contact.setAvatar(new Avatar(id, data)); + } + }); + } + +} diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index aa911b1f..45486a56 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -78,12 +78,13 @@ public enum Status { ERROR } + private final ViewControl mViewControl; + private final Client mClient; private final ChatStateManager mChatStateManager; private final AttachmentManager mAttachmentManager; private final RosterHandler mRosterHandler; - - private final ViewControl mViewControl; + private final AvatarHandler mAvatarHandler; private Status mCurrentStatus = Status.DISCONNECTED; @@ -94,12 +95,17 @@ private Control(Path appDir) { mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(appDir, this); mRosterHandler = new RosterHandler(this, mClient); + mAvatarHandler = new AvatarHandler(mClient); } public RosterHandler getRosterHandler() { return mRosterHandler; } + public AvatarHandler getAvatarHandler() { + return mAvatarHandler; + } + ViewControl getViewControl() { return mViewControl; } From 201503f8fbd5cef9b9455cd1ce83b6ebb6dbbfef Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Dec 2015 18:04:59 +0100 Subject: [PATCH 037/257] client: muliple minor improvements --- .../org/kontalk/client/BlockListListener.java | 8 +++++--- .../org/kontalk/client/BlockSendReceiver.java | 15 ++------------- src/main/java/org/kontalk/client/HKPClient.java | 4 ++-- .../java/org/kontalk/client/PresenceListener.java | 12 +++++++----- .../org/kontalk/client/PublicKeyListener.java | 10 ++++++---- .../java/org/kontalk/client/VCardListener.java | 9 +++++++-- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/kontalk/client/BlockListListener.java b/src/main/java/org/kontalk/client/BlockListListener.java index 93904ae5..cd61c3ee 100644 --- a/src/main/java/org/kontalk/client/BlockListListener.java +++ b/src/main/java/org/kontalk/client/BlockListListener.java @@ -36,14 +36,16 @@ final class BlockListListener implements StanzaListener { private final Control mControl; - public BlockListListener(Control control) { - mControl = control; - + static { ProviderManager.addIQProvider(BlockingCommand.BLOCKLIST, BlockingCommand.NAMESPACE, new BlockingCommand.Provider()); } + BlockListListener(Control control) { + mControl = control; + } + @Override public void processPacket(Stanza packet) { BlockingCommand p = (BlockingCommand) packet; diff --git a/src/main/java/org/kontalk/client/BlockSendReceiver.java b/src/main/java/org/kontalk/client/BlockSendReceiver.java index e664c965..f095957e 100644 --- a/src/main/java/org/kontalk/client/BlockSendReceiver.java +++ b/src/main/java/org/kontalk/client/BlockSendReceiver.java @@ -18,9 +18,7 @@ package org.kontalk.client; -import java.util.logging.Level; import java.util.logging.Logger; -import org.jivesoftware.smack.ExceptionCallback; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.packet.IQ; @@ -50,22 +48,13 @@ final class BlockSendReceiver implements StanzaListener { mJID = jid; } - public void sendAndListen() { + void sendAndListen() { LOGGER.info("jid: "+mJID+" blocking="+mBlocking); String command = mBlocking ? BlockingCommand.BLOCK : BlockingCommand.UNBLOCK; BlockingCommand blockingCommand = new BlockingCommand(command, mJID.string()); - try { - mConn.sendIqWithResponseCallback(blockingCommand, this, new ExceptionCallback() { - @Override - public void processException(Exception exception) { - LOGGER.log(Level.WARNING, "exception response", exception); - } - }); - } catch (SmackException.NotConnectedException ex) { - LOGGER.log(Level.WARNING, "not connected", ex); - } + mConn.sendWithCallback(blockingCommand, this); } @Override diff --git a/src/main/java/org/kontalk/client/HKPClient.java b/src/main/java/org/kontalk/client/HKPClient.java index cd1645e5..2d8edeaa 100644 --- a/src/main/java/org/kontalk/client/HKPClient.java +++ b/src/main/java/org/kontalk/client/HKPClient.java @@ -40,8 +40,8 @@ public final class HKPClient { private static final Logger LOGGER = Logger.getLogger(HKPClient.class.getName()); - //private static final short DEFAULT_PORT = 11371; - private static final short DEFAULT_SSL_PORT = 443; + //private static final int DEFAULT_PORT = 11371; + //private static final int DEFAULT_SSL_PORT = 443; private static final int MAX_CONTENT_LENGTH = 9001; diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index f7722214..5968e6aa 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -66,7 +66,7 @@ public void processPacket(Stanza packet) { Presence presence = (Presence) packet; - JID jid = JID.bare(presence.getFrom()); + JID jid = JID.full(presence.getFrom()); ExtensionElement publicKeyExt = presence.getExtension( PublicKeyPresence.ELEMENT_NAME, @@ -98,12 +98,14 @@ public void processPacket(Stanza packet) { return; } - Presence bestPresence = mRoster.getPresence(jid.string()); - // NOTE: a delay extension is sometimes included, don't know why; // ignoring mode, always null anyway - mHandler.onPresenceUpdate(JID.bare(bestPresence.getFrom()), + // NOTE: using only the "best" presence to ignore unimportant updates + // from multiple clients + Presence bestPresence = mRoster.getPresence(jid.string()); + + mHandler.onPresenceUpdate(jid, bestPresence.getType(), bestPresence.getStatus()); @@ -116,7 +118,7 @@ public void processPacket(Stanza packet) { } } - ExtensionElement signatureExt = presence.getExtension( + ExtensionElement signatureExt = bestPresence.getExtension( PresenceSignature.ELEMENT_NAME, PresenceSignature.NAMESPACE); if (signatureExt instanceof PresenceSignature) { diff --git a/src/main/java/org/kontalk/client/PublicKeyListener.java b/src/main/java/org/kontalk/client/PublicKeyListener.java index 1804cba1..2b9a0bf9 100644 --- a/src/main/java/org/kontalk/client/PublicKeyListener.java +++ b/src/main/java/org/kontalk/client/PublicKeyListener.java @@ -36,12 +36,14 @@ public class PublicKeyListener implements StanzaListener { private final Control mControl; + static { + ProviderManager.addIQProvider(PublicKeyPublish.ELEMENT_NAME, + PublicKeyPublish.NAMESPACE, + new PublicKeyPublish.Provider()); + } + public PublicKeyListener(Control control) { mControl = control; - - ProviderManager.addIQProvider(PublicKeyPublish.ELEMENT_NAME, - PublicKeyPublish.NAMESPACE, - new PublicKeyPublish.Provider()); } @Override diff --git a/src/main/java/org/kontalk/client/VCardListener.java b/src/main/java/org/kontalk/client/VCardListener.java index 5ff07b9c..303cf70b 100644 --- a/src/main/java/org/kontalk/client/VCardListener.java +++ b/src/main/java/org/kontalk/client/VCardListener.java @@ -24,10 +24,15 @@ final class VCardListener implements StanzaListener { private final Control mControl; + static { + ProviderManager.addIQProvider( + VCard4.ELEMENT_NAME, + VCard4.NAMESPACE, + new VCard4.Provider()); + } + VCardListener(Control control) { mControl = control; - - ProviderManager.addIQProvider(VCard4.ELEMENT_NAME, VCard4.NAMESPACE, new VCard4.Provider()); } @Override From 929ad8387c8f54f290018a9b87b973e504f653e3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 15 Dec 2015 17:51:50 +0100 Subject: [PATCH 038/257] control: refactored group control for future MUC support --- src/main/java/org/kontalk/system/Control.java | 142 +++++++----------- .../java/org/kontalk/system/GroupControl.java | 127 ++++++++++++++++ .../org/kontalk/system/RosterHandler.java | 2 +- .../java/org/kontalk/view/AvatarLoader.java | 32 +++- 4 files changed, 210 insertions(+), 93 deletions(-) create mode 100644 src/main/java/org/kontalk/system/GroupControl.java diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 45486a56..09fa5730 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -52,7 +52,6 @@ import org.kontalk.model.GroupChat; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.MessageContent.GroupCommand.OP; import org.kontalk.model.ProtoMessage; import org.kontalk.model.SingleChat; import org.kontalk.util.ClientUtils.MessageIDs; @@ -162,7 +161,7 @@ public boolean newInMessage(MessageIDs ids, MessageContent content) { LOGGER.info("new incoming message, "+ids); - Optional optContact = this.getOrCreate(ids.jid, ""); + Optional optContact = this.getOrCreateContact(ids.jid, ""); if (!optContact.isPresent()) { LOGGER.warning("can't get contact for message"); return false; @@ -178,9 +177,12 @@ public boolean newInMessage(MessageIDs ids, // note: decryption must be successful to select group chat Optional optGID = protoMessage.getContent().getGID(); + // TODO ignore message if it contains unexpected group commands + Chat chat = optGID.isPresent() ? ChatList.getInstance().getOrCreate(optGID.get(), contact) : ChatList.getInstance().getOrCreate(contact, ids.xmppThreadID); + InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, ids.xmppID, serverDate); @@ -199,12 +201,11 @@ public boolean newInMessage(MessageIDs ids, return false; } - Optional optCom = newMessage.getContent().getGroupCommand(); - if (optCom.isPresent()) { - if (chat instanceof GroupChat) - this.processGroupCommand(optCom.get(), (GroupChat) chat, contact); - else - LOGGER.warning("group command for non-group chat"); + if (chat instanceof GroupChat) { + this.GroupControlForChat((GroupChat) chat) + .onInMessage(newMessage.getContent(), contact); + } else if (newMessage.getContent().getGroupCommand().isPresent()) { + LOGGER.warning("group command for non-group chat"); } this.processContent(newMessage); @@ -339,6 +340,36 @@ public void setContactBlocking(JID jid, boolean blocking) { /* package */ + /** + * All-in-one method for a new outgoing message: Create, + * save, process and send message. + */ + boolean createAndSendMessage(Chat chat, MessageContent content) { + LOGGER.config("chat: "+chat+" content: "+content); + + if (!chat.isValid()) { + LOGGER.warning("invalid chat"); + return false; + } + + Contact[] contacts = chat.getValidContacts(); + if (contacts.length == 0) { + LOGGER.warning("can't send message, no (valid) contact(s)"); + return false; + } + + OutMessage newMessage = new OutMessage(chat, contacts, content, + chat.isSendEncrypted()); + if (newMessage.getContent().getAttachment().isPresent()) + mAttachmentManager.createImagePreview(newMessage); + boolean added = chat.addMessage(newMessage); + if (!added) { + LOGGER.warning("could not add outgoing message to chat"); + } + + return this.sendMessage(newMessage); + } + boolean sendMessage(OutMessage message) { if (message.getContent().getAttachment().isPresent() && !message.getContent().getAttachment().get().hasURL()) { @@ -366,7 +397,7 @@ void maySendKeyRequest(Contact contact) { mClient.sendPublicKeyRequest(contact.getJID()); } - Optional getOrCreate(JID jid, String name) { + Optional getOrCreateContact(JID jid, String name) { Optional optContact = ContactList.getInstance().get(jid); if (optContact.isPresent()) return optContact; @@ -464,34 +495,9 @@ private void removeFromRoster(JID jid) { } } - // warning: call this only once for each group command! - private void processGroupCommand(GroupCommand command, GroupChat chat, Contact sender) { - // note: chat was selected/created by GID so we can be sure message and - // chat GIDs match - GID gid = chat.getGID(); - OP op = command.getOperation(); - - // validation check - if (op != OP.LEAVE) { - // sender must be owner - if (!gid.ownerJID.equals(sender.getJID())) { - LOGGER.warning("sender not owner"); - return; - } - } - - if (op == OP.CREATE || op == OP.SET) { - // add contacts if necessary - // TODO design problem here: we need at least the public keys, but user - // might dont wanna have group members in contact list - for (JID jid : command.getAdded()) { - boolean succ = this.getOrCreate(jid, "").isPresent(); - if (!succ) - LOGGER.warning("can't create contact, JID: "+jid); - } - } - - chat.applyGroupCommand(command, sender); + private GroupControl GroupControlForChat(GroupChat chat) { + // TODO + return new GroupControl.KonChatControl(this, chat); } /* static */ @@ -676,30 +682,20 @@ public void createGroupChat(List contacts, String subject) { List withMe = new ArrayList<>(contacts); withMe.add(me); - Chat chat = ChatList.getInstance().createNew(withMe.toArray(new Contact[0]), - new GID(me.getJID(), - org.jivesoftware.smack.util.StringUtils.randomString(8)), + // TODO static new + GroupChat.GID gid = new GroupChat.GID(me.getJID(), org.jivesoftware.smack.util.StringUtils.randomString(8)); + + GroupChat chat = ChatList.getInstance().createNew(withMe.toArray(new Contact[0]), + gid, subject); - // send create group command - List jids = new ArrayList<>(contacts.size()); - for (Contact c: contacts) - jids.add(c.getJID()); - this.createAndSendMessage(chat, - MessageContent.groupCommand( - GroupCommand.create(jids.toArray(new JID[0]),subject) - ) - ); + Control.this.GroupControlForChat(chat).onCreate(contacts, subject); } public void deleteChat(Chat chat) { - if (chat.isGroupChat() && chat.isValid()) { - // note: group chats are not 'deleted', were just leaving them - boolean sent = this.createAndSendMessage(chat, - MessageContent.groupCommand(GroupCommand.leave())); - - if (!sent) - // TODO tell view (and/or delete chat when message was sent) + if (chat instanceof GroupChat) { + boolean succ = Control.this.GroupControlForChat((GroupChat) chat).beforeDelete(); + if (!succ) return; } @@ -711,7 +707,7 @@ public void setChatSubject(GroupChat chat, String subject) { LOGGER.warning("not admin"); return; } - this.createAndSendMessage(chat, MessageContent.groupCommand( + Control.this.createAndSendMessage(chat, MessageContent.groupCommand( GroupCommand.set(new JID[0], new JID[0], subject))); chat.setSubject(subject); @@ -753,7 +749,7 @@ private void sendTextMessage(Chat chat, String text, Path file) { MessageContent.plainText(text) : MessageContent.outgoing(text, attachment); - this.createAndSendMessage(chat, content); + Control.this.createAndSendMessage(chat, content); } private PersonalKey keyOrNull(char[] password) { @@ -780,36 +776,6 @@ private PersonalKey keyOrNull(char[] password) { } } - /** - * All-in-one method for a new outgoing message: Create, - * save, process and send message. - */ - private boolean createAndSendMessage(Chat chat, MessageContent content) { - LOGGER.config("chat: "+chat+" content: "+content); - - if (!chat.isValid()) { - LOGGER.warning("invalid chat"); - return false; - } - - Contact[] contacts = chat.getValidContacts(); - if (contacts.length == 0) { - LOGGER.warning("can't send message, no (valid) contact(s)"); - return false; - } - - OutMessage newMessage = new OutMessage(chat, contacts, content, - chat.isSendEncrypted()); - if (newMessage.getContent().getAttachment().isPresent()) - mAttachmentManager.createImagePreview(newMessage); - boolean added = chat.addMessage(newMessage); - if (!added) { - LOGGER.warning("could not add outgoing message to chat"); - } - - return Control.this.sendMessage(newMessage); - } - void changed(ViewEvent event) { this.setChanged(); this.notifyObservers(event); diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java new file mode 100644 index 00000000..dbb1871d --- /dev/null +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -0,0 +1,127 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.system; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; +import org.kontalk.misc.JID; +import org.kontalk.model.Contact; +import org.kontalk.model.GroupChat; +import org.kontalk.model.MessageContent; +import org.kontalk.model.MessageContent.GroupCommand; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +interface GroupControl { + + void onCreate(List contacts, String subject); + + void onSetSubject(String subject); + + boolean beforeDelete(); + + // warning: call this only once for each group command! + void onInMessage(MessageContent content, Contact sender); + + class KonChatControl implements GroupControl { + private static final Logger LOGGER = Logger.getLogger(KonChatControl.class.getName()); + + private final Control mControl; + private final GroupChat mChat; + + KonChatControl(Control control, GroupChat chat) { + mControl = control; + mChat = chat; + } + + @Override + public void onCreate(List contacts, String subject) { + // send create group command + List jids = new ArrayList<>(contacts.size()); + for (Contact c: contacts) + jids.add(c.getJID()); + + mControl.createAndSendMessage(mChat, + MessageContent.groupCommand( + MessageContent.GroupCommand.create(jids.toArray(new JID[0]),subject) + ) + ); + } + + @Override + public void onSetSubject(String subject) { + mControl.createAndSendMessage(mChat, MessageContent.groupCommand( + MessageContent.GroupCommand.set(new JID[0], new JID[0], subject))); + } + + @Override + public boolean beforeDelete() { + if (!mChat.isValid()) + return true; + + // note: group chats are not 'deleted', were just leaving them + return mControl.createAndSendMessage(mChat, + MessageContent.groupCommand(GroupCommand.leave())); + } + + @Override + public void onInMessage(MessageContent content, Contact sender) { + Optional optCom = content.getGroupCommand(); + if (!optCom.isPresent()) { + return; + } + + // TODO ignore message if it contains unexpected group command + + GroupCommand command = optCom.get(); + + // NOTE: chat was selected/created by GID so we can be sure message and + // chat GIDs match + GroupChat.GID gid = mChat.getGID(); + MessageContent.GroupCommand.OP op = command.getOperation(); + + // validation check + if (op != MessageContent.GroupCommand.OP.LEAVE) { + // sender must be owner + if (!gid.ownerJID.equals(sender.getJID())) { + LOGGER.warning("sender not owner"); + return; + } + } + + if (op == MessageContent.GroupCommand.OP.CREATE || + op == MessageContent.GroupCommand.OP.SET) { + // add contacts if necessary + // TODO design problem here: we need at least the public keys, but user + // might dont wanna have group members in contact list + for (JID jid : command.getAdded()) { + boolean succ = mControl.getOrCreateContact(jid, "").isPresent(); + if (!succ) + LOGGER.warning("can't create contact, JID: "+jid); + } + } + + mChat.applyGroupCommand(command, sender); + } + } +} diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index ee654fa2..c59e564b 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -117,7 +117,7 @@ public void onEntryUpdate(JID jid, } public void onSubscriptionRequest(JID jid, byte[] rawKey) { - Optional optContact = mControl.getOrCreate(jid, ""); + Optional optContact = mControl.getOrCreateContact(jid, ""); if (!optContact.isPresent()) return; Contact contact = optContact.get(); diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 511b04fc..0816b924 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -18,18 +18,22 @@ package org.kontalk.view; +import com.alee.utils.ImageUtils; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Map; import java.util.Objects; import javax.swing.ImageIcon; +import org.apache.commons.lang.ObjectUtils; import org.kontalk.model.Chat; import org.kontalk.model.Contact; +import org.kontalk.model.Contact.Avatar; /** * Static functions for loading avatar pictures. @@ -57,8 +61,15 @@ static ImageIcon load(Contact contact) { private static ImageIcon load(Item item) { if (!CACHE.containsKey(item)) { - // TODO - CACHE.put(item, new ImageIcon(fallback(item.label, item.colorCode, IMG_SIZE))); + ImageIcon icon = null; + + if (item.avatar != null) + icon = ImageUtils.loadImage(new ByteArrayInputStream(item.avatar.data)); + + if (icon == null) + icon = new ImageIcon(fallback(item.label, item.colorCode, IMG_SIZE)); + + CACHE.put(item, icon); } return CACHE.get(item); } @@ -105,20 +116,32 @@ private static BufferedImage fallback(String text, int colorCode, int size) { } private static class Item { + final Avatar avatar; final String label; final int colorCode; Item(Contact contact) { + avatar = contact.getAvatar().orElse(null); label = contact.getName(); colorCode = hash(contact.getID()); } Item(Chat chat) { if (chat.isGroupChat()) { + // nice to have: group picture + avatar = null; label = chat.getSubject(); } else { Contact[] contacts = chat.getValidContacts(); - label = contacts.length > 0 ? contacts[0].getName() : ""; + if (contacts.length == 0) { + avatar = null; + label = ""; + } else { + Contact c = contacts[0]; + avatar = c.getAvatar().orElse(null); + label = c.getName(); + } + } colorCode = hash(chat.getID()); } @@ -132,7 +155,8 @@ public boolean equals(Object o) { return false; Item oItem = (Item) o; - return label.equals(oItem.label) && colorCode == oItem.colorCode; + return ObjectUtils.equals(avatar, oItem.avatar) && + label.equals(oItem.label) && colorCode == oItem.colorCode; } @Override From bcc06c68e90a9bb5069369e07b2413835ee5d2d9 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Dec 2015 15:49:10 +0100 Subject: [PATCH 039/257] model: support for MUC chats --- .../kontalk/client/KonMessageListener.java | 8 +- .../org/kontalk/client/KonMessageSender.java | 9 +- src/main/java/org/kontalk/model/Chat.java | 24 +-- src/main/java/org/kontalk/model/ChatList.java | 22 +-- .../java/org/kontalk/model/GroupChat.java | 103 +++++------- .../java/org/kontalk/model/GroupMetaData.java | 149 ++++++++++++++++++ .../org/kontalk/model/MessageContent.java | 22 +-- src/main/java/org/kontalk/system/Control.java | 14 +- .../java/org/kontalk/system/Database.java | 2 +- .../java/org/kontalk/system/GroupControl.java | 13 +- .../java/org/kontalk/util/ClientUtils.java | 8 +- 11 files changed, 254 insertions(+), 120 deletions(-) create mode 100644 src/main/java/org/kontalk/model/GroupMetaData.java diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 481f59dd..43c08654 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -43,7 +43,7 @@ import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.misc.JID; -import org.kontalk.model.GroupChat.GID; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; @@ -269,13 +269,13 @@ public static MessageContent parseMessageContent(Message m) { } // group command - GID gid = null; + KonGroupData gid = null; GroupCommand groupCommand = null; ExtensionElement groupExt = m.getExtension(GroupExtension.ELEMENT_NAME, GroupExtension.NAMESPACE); if (groupExt instanceof GroupExtension) { GroupExtension group = (GroupExtension) groupExt; - gid = new GID(JID.bare(group.getOwner()), group.getID()); + gid = new KonGroupData(JID.bare(group.getOwner()), group.getID()); groupCommand = ClientUtils.groupExtensionToGroupCommand( group.getCommand(), group.getMember(), group.getSubject()).orElse(null); } @@ -283,7 +283,7 @@ public static MessageContent parseMessageContent(Message m) { return new MessageContent.Builder(plainText, encrypted) .attachment(attachment) .preview(preview) - .gid(gid) + .groupData(gid) .groupCommand(groupCommand).build(); } } diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 5cbc28d1..4d3ad2c0 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -28,7 +28,8 @@ import org.kontalk.crypto.Coder; import org.kontalk.misc.JID; import org.kontalk.model.Chat; -import org.kontalk.model.GroupChat; +import org.kontalk.model.GroupChat.KonGroupChat; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.KonMessage; import org.kontalk.model.MessageContent; import org.kontalk.model.OutMessage; @@ -154,9 +155,9 @@ private static Message rawMessage(MessageContent content, Chat chat, boolean enc } // group command - if (chat instanceof GroupChat) { - GroupChat groupChat = (GroupChat) chat; - GroupChat.GID gid = groupChat.getGID(); + if (chat instanceof KonGroupChat) { + KonGroupChat groupChat = (KonGroupChat) chat; + KonGroupData gid = groupChat.getGroupData(); Optional optGroupCommand = content.getGroupCommand(); smackMessage.addExtension(optGroupCommand.isPresent() ? ClientUtils.groupCommandToGroupExtension(groupChat, optGroupCommand.get()) : diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index d0322fc8..f9c1b1b2 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -37,7 +37,7 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.json.simple.JSONObject; import org.json.simple.JSONValue; -import org.kontalk.model.GroupChat.GID; +import org.kontalk.model.GroupMetaData; import org.kontalk.system.Database; /** @@ -52,7 +52,7 @@ public abstract class Chat extends Observable implements Observer { public static final String TABLE = "threads"; public static final String COL_XMPPID = "xmpp_id"; - public static final String COL_GID = "gid"; + public static final String COL_GD = "gid"; public static final String COL_SUBJ = "subject"; public static final String COL_READ = "read"; public static final String COL_VIEW_SET = "view_settings"; @@ -67,7 +67,7 @@ public abstract class Chat extends Observable implements Observer { // view settings in JSON format COL_VIEW_SET+" TEXT NOT NULL, " + // optional group id in JSON format - COL_GID+" TEXT " + + COL_GD+" TEXT " + ")"; // many to many relationship requires additional table for receiver @@ -94,7 +94,7 @@ protected Chat(Contact contact, String xmppID, String subject) { this(new Contact[]{contact}, xmppID, subject, null); } - protected Chat(Contact[] contacts, String xmppID, String subject, GID gid) { + protected Chat(Contact[] contacts, String xmppID, String subject, GroupMetaData gData) { mMessages = new ChatMessages(this, true); mRead = true; mViewSettings = new ViewSettings(); @@ -106,7 +106,7 @@ protected Chat(Contact[] contacts, String xmppID, String subject, GID gid) { values.add(Database.setString(subject)); values.add(mRead); values.add(mViewSettings.toJSONString()); - values.add(Database.setString(gid == null ? "" : gid.toJSON())); + values.add(Database.setString(gData == null ? "" : gData.toJSON())); mID = db.execInsert(TABLE, values); if (mID < 1) { LOGGER.warning("couldn't insert chat"); @@ -130,6 +130,8 @@ public ChatMessages getMessages() { } public boolean addMessage(KonMessage message) { + assert message.getChat() == this; + boolean added = mMessages.add(message); if (added) { if (message.isInMessage() && mRead) { @@ -173,7 +175,7 @@ public void setViewSettings(ViewSettings settings) { } public boolean isGroupChat() { - return this instanceof GroupChat; + return (this instanceof GroupChat); } /** Get all contacts (including deleted, blocked and user contact). */ @@ -283,10 +285,10 @@ public void update(Observable o, Object arg) { static Chat loadOrNull(ResultSet rs) throws SQLException { int id = rs.getInt("_id"); - String jsonGID = Database.getString(rs, Chat.COL_GID); - Optional optGID = Optional.ofNullable(jsonGID.isEmpty() ? + String jsonGD = Database.getString(rs, Chat.COL_GD); + Optional optGD = Optional.ofNullable(jsonGD.isEmpty() ? null : - GID.fromJSONOrNull(jsonGID)); + GroupMetaData.fromJSONOrNull(jsonGD)); String xmppID = Database.getString(rs, Chat.COL_XMPPID); @@ -308,8 +310,8 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String jsonViewSettings = Database.getString(rs, Chat.COL_VIEW_SET); - if (optGID.isPresent()) { - return new GroupChat(id, contacts, optGID.get(), subject, read, jsonViewSettings); + if (optGD.isPresent()) { + return GroupChat.create(id, contacts, optGD.get(), subject, read, jsonViewSettings); } else { if (contacts.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index 588643c8..deefb29a 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -31,7 +31,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import org.kontalk.model.GroupChat.GID; +import org.kontalk.model.GroupMetaData; import org.kontalk.system.Database; /** @@ -88,13 +88,13 @@ public Optional get(Contact contact, String xmmpThreadID) { } /** Get group chat with group ID and containing contact. */ - public Optional get(GID gid, Contact contact) { + public Optional get(GroupMetaData gData, Contact contact) { for (Chat chat : mMap.values()) { if (!(chat instanceof GroupChat)) continue; GroupChat groupChat = (GroupChat) chat; - if (groupChat.getGID().equals(gid) && + if (groupChat.getGroupData().equals(gData) && groupChat.getAllContacts().contains(contact)) { return Optional.of(groupChat); } @@ -107,13 +107,13 @@ public Chat getOrCreate(Contact contact) { return this.getOrCreate(contact, ""); } - /** Find group chat by GID or create a new chat. */ - public GroupChat getOrCreate(GID gid, Contact contact) { - Optional optChat = this.get(gid, contact); + /** Find group chat by group data or create a new chat. */ + public GroupChat getOrCreate(GroupMetaData gData, Contact contact) { + Optional optChat = this.get(gData, contact); if (optChat.isPresent()) return optChat.get(); - return this.createNew(new Contact[]{contact}, gid, ""); + return this.createNew(new Contact[]{contact}, gData, ""); } /** Find single chat for contact and XMPP ID or creates a new chat. */ @@ -127,15 +127,15 @@ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { private SingleChat createNew(Contact contact, String xmppThreadID) { SingleChat newChat = new SingleChat(contact, xmppThreadID); - LOGGER.config("created new single chat: "+newChat); + LOGGER.config("new single chat: "+newChat); this.putSilent(newChat); this.changed(newChat); return newChat; } - public GroupChat createNew(Contact[] contacts, GID gid, String subject) { - GroupChat newChat = new GroupChat(contacts, gid, subject); - LOGGER.config("created new group chat: "+newChat); + public GroupChat createNew(Contact[] contacts, GroupMetaData gData, String subject) { + GroupChat newChat = GroupChat.create(contacts, gData, subject); + LOGGER.config("new group chat: "+newChat); this.putSilent(newChat); this.changed(newChat); return newChat; diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 6d5e753d..15fab208 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -20,37 +20,35 @@ import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; -import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; import org.kontalk.misc.JID; -import org.kontalk.util.EncodingUtils; +import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.GroupMetaData.MUCData; /** + * A long-term persistent chat conversation with multiple participants. * * @author Alexander Bikadorov {@literal } */ -public final class GroupChat extends Chat { +public abstract class GroupChat extends Chat { private static final Logger LOGGER = Logger.getLogger(GroupChat.class.getName()); private final HashMap mContactMap = new HashMap<>(); - private final GID mGID; + private final D mGroupData; private String mSubject; // TODO overwrite encryption=OFF field private boolean mForceEncryptionOff = false; - GroupChat(Contact[] contacts, GID gid, String subject) { - super(contacts, "", subject, gid); + private GroupChat(Contact[] contacts, D gData, String subject) { + super(contacts, "", subject, gData); - mGID = gid; + mGroupData = gData; mSubject = subject; for (Contact contact: contacts) @@ -58,16 +56,16 @@ public final class GroupChat extends Chat { } // used when loading from database - GroupChat(int id, + private GroupChat(int id, Set contacts, - GID gid, + D gData, String subject, boolean read, String jsonViewSettings ) { super(id, read, jsonViewSettings); - mGID = gid; + mGroupData = gData; mSubject = subject; for (Contact contact: contacts) @@ -118,8 +116,8 @@ private void removeContactSilent(Contact contact) { this.save(); } - public GID getGID() { - return mGID; + public D getGroupData() { + return mGroupData; } @Override @@ -238,7 +236,7 @@ public boolean isValid() { @Override public boolean isAdministratable() { - return mGID.ownerJID.isMe(); + return mGroupData.isAdministratable(); } private boolean containsMe() { @@ -264,72 +262,51 @@ public boolean equals(Object o) { if (!(o instanceof GroupChat)) return false; GroupChat oChat = (GroupChat) o; - return mGID.equals(oChat.mGID); + return mGroupData.equals(oChat.mGroupData); } @Override public int hashCode() { int hash = 7; - hash = 79 * hash + Objects.hashCode(this.mGID); + hash = 79 * hash + Objects.hashCode(this.mGroupData); return hash; } @Override public String toString() { - return "GC:id="+mID+",gid="+mGID+",subject="+mSubject; + return "GC:id="+mID+",gd="+mGroupData+",subject="+mSubject; } - /** Group ID. */ - public static class GID { - private static final String JSON_OWNER_JID = "jid"; - private static final String JSON_ID = "id"; - - public final JID ownerJID; - public final String id; - - public GID(JID ownerJID, String id) { - this.ownerJID = ownerJID; - this.id = id; + public static final class KonGroupChat extends GroupChat { + KonGroupChat(Contact[] contacts, KonGroupData gData, String subject) { + super(contacts, gData, subject); } - // using legacy lib, raw types extend Object - @SuppressWarnings("unchecked") - protected String toJSON() { - JSONObject json = new JSONObject(); - EncodingUtils.putJSON(json, JSON_OWNER_JID, ownerJID.string()); - EncodingUtils.putJSON(json, JSON_ID, id); - return json.toJSONString(); + KonGroupChat(int id, Set contacts, KonGroupData gData, String subject, boolean read, String jsonViewSettings) { + super(id, contacts, gData, subject, read, jsonViewSettings); } + } - static GID fromJSONOrNull(String json) { - Object obj = JSONValue.parse(json); - try { - Map map = (Map) obj; - JID jid = JID.bare((String) map.get(JSON_OWNER_JID)); - String id = (String) map.get(JSON_ID); - return new GID(jid, id); - } catch (NullPointerException | ClassCastException ex) { - LOGGER.log(Level.WARNING, "can't parse JSON preview", ex); - return null; - } + public static final class MUCChat extends GroupChat { + private MUCChat(Contact[] contacts, GroupMetaData.MUCData gData, String subject) { + super(contacts, gData, subject); } - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (!(o instanceof GID)) return false; - - GID oGID = (GID) o; - return ownerJID.equals(oGID.ownerJID) && id.equals(oGID.id); + private MUCChat(int id, Set contacts, GroupMetaData.MUCData gData, String subject, boolean read, String jsonViewSettings) { + super(id, contacts, gData, subject, read, jsonViewSettings); } + } - @Override - public int hashCode() { - int hash = 7; - hash = 37 * hash + Objects.hashCode(this.ownerJID); - hash = 37 * hash + Objects.hashCode(this.id); - return hash; - } + static GroupChat create(int id, Set contacts, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { + return (gData instanceof KonGroupData) ? + new KonGroupChat(id, contacts, (KonGroupData) gData, subject, read, jsonViewSettings) : + new MUCChat(id, contacts, (MUCData) gData, subject, read, jsonViewSettings); } + + public static GroupChat create(Contact[] contacts, GroupMetaData gData, String subject) { + return (gData instanceof KonGroupData) ? + new KonGroupChat(contacts, (KonGroupData) gData, subject) : + new MUCChat(contacts, (MUCData) gData, subject); + } + } diff --git a/src/main/java/org/kontalk/model/GroupMetaData.java b/src/main/java/org/kontalk/model/GroupMetaData.java new file mode 100644 index 00000000..a737545e --- /dev/null +++ b/src/main/java/org/kontalk/model/GroupMetaData.java @@ -0,0 +1,149 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.model; + +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.kontalk.misc.JID; + +/** + * Immutable meta data fields for a specific group chat protocol implementation. + * + * TODO toString() methods + * + * @author Alexander Bikadorov {@literal } + */ +public abstract class GroupMetaData { + private static final Logger LOGGER = Logger.getLogger(GroupMetaData.class.getName()); + + // TODO move role/affiliation data to group chat class + abstract boolean isAdministratable(); + + abstract String toJSON(); + + /** Data fields specific to a Kontalk group chat (custom protocol). */ + public static class KonGroupData extends GroupMetaData { + private static final String JSON_OWNER_JID = "jid"; + private static final String JSON_ID = "id"; + + public final JID ownerJID; + public final String id; + + public KonGroupData(JID ownerJID, String id) { + this.ownerJID = ownerJID; + this.id = id; + } + + @Override + boolean isAdministratable() { + return ownerJID.isMe(); + } + + // using legacy lib, raw types extend Object + @SuppressWarnings(value = "unchecked") + @Override + public String toJSON() { + JSONObject json = new JSONObject(); + json.put(JSON_OWNER_JID, ownerJID.string()); + json.put(JSON_ID, id); + return json.toJSONString(); + } + + private static GroupMetaData fromJSON(Map map) { + JID jid = JID.bare((String) map.get(JSON_OWNER_JID)); + String id = (String) map.get(JSON_ID); + return new KonGroupData(jid, id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KonGroupData)) { + return false; + } + KonGroupData oGID = (KonGroupData) o; + return ownerJID.equals(oGID.ownerJID) && id.equals(oGID.id); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 37 * hash + Objects.hashCode(this.ownerJID); + hash = 37 * hash + Objects.hashCode(this.id); + return hash; + } + } + + /** Data fields specific to a MUC (XEP-0045) chat. */ + public static class MUCData extends GroupMetaData { + private static final String JSON_ROOM = "room"; + private static final String JSON_PW = "pw"; + + public final JID room; + public final String password; + + public MUCData(JID room, String password) { + this.room = room; + this.password = password; + } + + @Override + boolean isAdministratable() { + // TODO + return true; + } + + // using legacy lib, raw types extend Object + @SuppressWarnings(value = "unchecked") + @Override + public String toJSON() { + JSONObject json = new JSONObject(); + json.put(JSON_ROOM, room.string()); + json.put(JSON_PW, password); + return json.toJSONString(); + } + + private static GroupMetaData fromJSON(Map map) { + JID room = JID.bare((String) map.get(JSON_ROOM)); + String pw = (String) map.get(JSON_PW); + return new MUCData(room, pw); + } + + // TODO equals() + } + + static GroupMetaData fromJSONOrNull(String json) { + Object obj = JSONValue.parse(json); + try { + Map map = (Map) obj; + return map.containsKey(MUCData.JSON_ROOM) ? + MUCData.fromJSON(map) : + KonGroupData.fromJSON(map); + } catch (NullPointerException | ClassCastException ex) { + LOGGER.log(Level.WARNING, "can't parse JSON", ex); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/kontalk/model/MessageContent.java b/src/main/java/org/kontalk/model/MessageContent.java index be94658f..ddd2c188 100644 --- a/src/main/java/org/kontalk/model/MessageContent.java +++ b/src/main/java/org/kontalk/model/MessageContent.java @@ -32,7 +32,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.crypto.Coder; -import org.kontalk.model.GroupChat.GID; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.util.EncodingUtils; /** @@ -53,7 +53,7 @@ public class MessageContent { // small preview file of attachment private Optional mOptPreview; // group id - private final Optional mOptGID; + private final Optional mOptGroupData; // group command private final Optional mOptGroupCommand; // decrypted message content @@ -88,7 +88,7 @@ private MessageContent(Builder builder) { mEncryptedContent = builder.mEncrypted; mOptAttachment = Optional.ofNullable(builder.mAttachment); mOptPreview = Optional.ofNullable(builder.mPreview); - mOptGID = Optional.ofNullable(builder.mGID); + mOptGroupData = Optional.ofNullable(builder.mGroupData); mOptGroupCommand = Optional.ofNullable(builder.mGroup); mOptDecryptedContent = Optional.ofNullable(builder.mDecrypted); } @@ -144,12 +144,12 @@ void setPreview(Preview preview) { mOptPreview = Optional.of(preview); } - public Optional getGID() { + public Optional getGroupData() { if (mOptDecryptedContent.isPresent() && - mOptDecryptedContent.get().getGID().isPresent()) { - return mOptDecryptedContent.get().getGID(); + mOptDecryptedContent.get().getGroupData().isPresent()) { + return mOptDecryptedContent.get().getGroupData(); } - return mOptGID; + return mOptGroupData; } public Optional getGroupCommand() { @@ -180,7 +180,7 @@ public boolean isComplex() { @Override public String toString() { return "CONT:plain="+mPlainText+",encr="+mEncryptedContent - +",att="+mOptAttachment+",gid="+mOptGID+",gc="+mOptGroupCommand + +",att="+mOptAttachment+",gd="+mOptGroupData+",gc="+mOptGroupCommand +",decr="+mOptDecryptedContent; } @@ -595,7 +595,7 @@ public static class Builder { private Attachment mAttachment = null; private Preview mPreview = null; - private GID mGID = null; + private KonGroupData mGroupData = null; private GroupCommand mGroup = null; private MessageContent mDecrypted = null; @@ -608,8 +608,8 @@ public Builder attachment(Attachment attachment) { mAttachment = attachment; return this; }; public Builder preview(Preview preview) { mPreview = preview; return this; }; - public Builder gid(GID gid) { - mGID = gid; return this; }; + public Builder groupData(KonGroupData gData) { + mGroupData = gData; return this; }; public Builder groupCommand(GroupCommand group) { mGroup = group; return this; }; private Builder decryptedContent(MessageContent decrypted) { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 09fa5730..aa47878b 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -42,7 +42,6 @@ import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; import org.kontalk.model.Chat; -import org.kontalk.model.GroupChat.GID; import org.kontalk.model.MessageContent; import org.kontalk.model.OutMessage; import org.kontalk.model.ChatList; @@ -50,6 +49,9 @@ import org.kontalk.model.ContactList; import org.kontalk.misc.JID; import org.kontalk.model.GroupChat; +import org.kontalk.model.GroupChat.KonGroupChat; +import org.kontalk.model.GroupMetaData; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.ProtoMessage; @@ -175,7 +177,7 @@ public boolean newInMessage(MessageIDs ids, } // note: decryption must be successful to select group chat - Optional optGID = protoMessage.getContent().getGID(); + Optional optGID = protoMessage.getContent().getGroupData(); // TODO ignore message if it contains unexpected group commands @@ -496,8 +498,10 @@ private void removeFromRoster(JID jid) { } private GroupControl GroupControlForChat(GroupChat chat) { - // TODO - return new GroupControl.KonChatControl(this, chat); + return (chat instanceof KonGroupChat) ? + new GroupControl.KonChatControl(this, (KonGroupChat) chat) : + // TODO + null; } /* static */ @@ -683,7 +687,7 @@ public void createGroupChat(List contacts, String subject) { withMe.add(me); // TODO static new - GroupChat.GID gid = new GroupChat.GID(me.getJID(), org.jivesoftware.smack.util.StringUtils.randomString(8)); + GroupMetaData gid = new GroupMetaData.KonGroupData(me.getJID(), org.jivesoftware.smack.util.StringUtils.randomString(8)); GroupChat chat = ChatList.getInstance().createNew(withMe.toArray(new Contact[0]), gid, diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index e4b4e333..31faf86c 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -173,7 +173,7 @@ private void update(int fromVersion) throws SQLException { mConn.createStatement().execute("PRAGMA foreign_keys=ON"); mConn.createStatement().execute("ALTER TABLE "+Chat.TABLE+ - " ADD COLUMN "+Chat.COL_GID+" DEFAULT NULL"); + " ADD COLUMN "+Chat.COL_GD+" DEFAULT NULL"); } // set new version diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index dbb1871d..fc848a60 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -24,7 +24,8 @@ import java.util.logging.Logger; import org.kontalk.misc.JID; import org.kontalk.model.Contact; -import org.kontalk.model.GroupChat; +import org.kontalk.model.GroupChat.KonGroupChat; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.GroupCommand; @@ -47,9 +48,9 @@ class KonChatControl implements GroupControl { private static final Logger LOGGER = Logger.getLogger(KonChatControl.class.getName()); private final Control mControl; - private final GroupChat mChat; + private final KonGroupChat mChat; - KonChatControl(Control control, GroupChat chat) { + KonChatControl(Control control, KonGroupChat chat) { mControl = control; mChat = chat; } @@ -95,9 +96,9 @@ public void onInMessage(MessageContent content, Contact sender) { GroupCommand command = optCom.get(); - // NOTE: chat was selected/created by GID so we can be sure message and - // chat GIDs match - GroupChat.GID gid = mChat.getGID(); + // NOTE: chat was selected/created by GID so we can be sure message + // and chat GIDs match + KonGroupData gid = mChat.getGroupData(); MessageContent.GroupCommand.OP op = command.getOperation(); // validation check diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 4a1115e7..1b504640 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -29,10 +29,10 @@ import org.kontalk.client.GroupExtension; import org.kontalk.client.GroupExtension.Command; import org.kontalk.client.GroupExtension.Member; -import org.kontalk.model.GroupChat.GID; import org.kontalk.model.Contact; import org.kontalk.misc.JID; -import org.kontalk.model.GroupChat; +import org.kontalk.model.GroupChat.KonGroupChat; +import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.MessageContent.GroupCommand.OP; @@ -77,11 +77,11 @@ public String toString() { } } - public static GroupExtension groupCommandToGroupExtension(GroupChat chat, + public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, GroupCommand groupCommand) { assert chat.isGroupChat(); - GID gid = chat.getGID(); + KonGroupData gid = chat.getGroupData(); OP op = groupCommand.getOperation(); switch (op) { case LEAVE: From 208d0e495149acde79a1674453832f28869fe970 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 6 Jan 2016 19:06:40 +0100 Subject: [PATCH 040/257] model: group data improved --- .../org/kontalk/client/KonMessageSender.java | 2 +- .../java/org/kontalk/model/GroupMetaData.java | 45 ++++++++++++++++--- .../java/org/kontalk/system/GroupControl.java | 2 +- .../java/org/kontalk/util/ClientUtils.java | 6 ++- 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 4d3ad2c0..7db75135 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -161,7 +161,7 @@ private static Message rawMessage(MessageContent content, Chat chat, boolean enc Optional optGroupCommand = content.getGroupCommand(); smackMessage.addExtension(optGroupCommand.isPresent() ? ClientUtils.groupCommandToGroupExtension(groupChat, optGroupCommand.get()) : - new GroupExtension(gid.id, gid.ownerJID.string())); + new GroupExtension(gid.id, gid.owner.string())); } return smackMessage; diff --git a/src/main/java/org/kontalk/model/GroupMetaData.java b/src/main/java/org/kontalk/model/GroupMetaData.java index a737545e..c2a277db 100644 --- a/src/main/java/org/kontalk/model/GroupMetaData.java +++ b/src/main/java/org/kontalk/model/GroupMetaData.java @@ -46,17 +46,17 @@ public static class KonGroupData extends GroupMetaData { private static final String JSON_OWNER_JID = "jid"; private static final String JSON_ID = "id"; - public final JID ownerJID; + public final JID owner; public final String id; public KonGroupData(JID ownerJID, String id) { - this.ownerJID = ownerJID; + this.owner = ownerJID; this.id = id; } @Override boolean isAdministratable() { - return ownerJID.isMe(); + return owner.isMe(); } // using legacy lib, raw types extend Object @@ -64,7 +64,7 @@ boolean isAdministratable() { @Override public String toJSON() { JSONObject json = new JSONObject(); - json.put(JSON_OWNER_JID, ownerJID.string()); + json.put(JSON_OWNER_JID, owner.string()); json.put(JSON_ID, id); return json.toJSONString(); } @@ -84,16 +84,21 @@ public boolean equals(Object o) { return false; } KonGroupData oGID = (KonGroupData) o; - return ownerJID.equals(oGID.ownerJID) && id.equals(oGID.id); + return owner.equals(oGID.owner) && id.equals(oGID.id); } @Override public int hashCode() { int hash = 7; - hash = 37 * hash + Objects.hashCode(this.ownerJID); + hash = 37 * hash + Objects.hashCode(this.owner); hash = 37 * hash + Objects.hashCode(this.id); return hash; } + + @Override + public String toString() { + return "KGD:{id="+id+",owner="+owner+"}"; + } } /** Data fields specific to a MUC (XEP-0045) chat. */ @@ -104,6 +109,10 @@ public static class MUCData extends GroupMetaData { public final JID room; public final String password; + public MUCData(JID room) { + this(room, ""); + } + public MUCData(JID room, String password) { this.room = room; this.password = password; @@ -131,7 +140,29 @@ private static GroupMetaData fromJSON(Map map) { return new MUCData(room, pw); } - // TODO equals() + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MUCData)) { + return false; + } + MUCData oData = (MUCData) o; + return room.equals(oData.room); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.room); + return hash; + } + + @Override + public String toString() { + return "MUCD:{room="+room+"}"; + } } static GroupMetaData fromJSONOrNull(String json) { diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index fc848a60..454eae65 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -104,7 +104,7 @@ public void onInMessage(MessageContent content, Contact sender) { // validation check if (op != MessageContent.GroupCommand.OP.LEAVE) { // sender must be owner - if (!gid.ownerJID.equals(sender.getJID())) { + if (!gid.owner.equals(sender.getJID())) { LOGGER.warning("sender not owner"); return; } diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 1b504640..8697c158 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -77,6 +77,7 @@ public String toString() { } } + /* Internal to external */ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, GroupCommand groupCommand) { assert chat.isGroupChat(); @@ -86,7 +87,7 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, switch (op) { case LEAVE: // weare leaving - return new GroupExtension(gid.id, gid.ownerJID.string(), Command.LEAVE); + return new GroupExtension(gid.id, gid.owner.string(), Command.LEAVE); case CREATE: case SET: Command command; @@ -118,7 +119,7 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, } return new GroupExtension(gid.id, - gid.ownerJID.string(), + gid.owner.string(), command, member.toArray(new Member[0]), subject); @@ -128,6 +129,7 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, } } + /* External to internal */ public static Optional groupExtensionToGroupCommand( Command com, Member[] members, From 7b8ae0141678c905c9932b72e3853c4feade31f0 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 8 Jan 2016 18:20:33 +0100 Subject: [PATCH 041/257] model: new method to get chat by group chat data --- src/main/java/org/kontalk/model/ChatList.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index deefb29a..f8df2f49 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -103,8 +103,18 @@ public Optional get(GroupMetaData gData, Contact contact) { return Optional.empty(); } - public Chat getOrCreate(Contact contact) { - return this.getOrCreate(contact, ""); + public Optional get(GroupMetaData gData) { + for (Chat chat : mMap.values()) { + if (!(chat instanceof GroupChat)) + continue; + + GroupChat groupChat = (GroupChat) chat; + if (groupChat.getGroupData().equals(gData)) + return Optional.of(groupChat); + + } + + return Optional.empty(); } /** Find group chat by group data or create a new chat. */ @@ -116,6 +126,10 @@ public GroupChat getOrCreate(GroupMetaData gData, Contact contact) { return this.createNew(new Contact[]{contact}, gData, ""); } + public Chat getOrCreate(Contact contact) { + return this.getOrCreate(contact, ""); + } + /** Find single chat for contact and XMPP ID or creates a new chat. */ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { Optional optChat = this.get(contact, xmppThreadID); From 61682c40f973f7b65e3dddbfed551bd3870ed1e4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 8 Jan 2016 18:24:59 +0100 Subject: [PATCH 042/257] model: new methods to add contacts to group chat --- .../java/org/kontalk/model/GroupChat.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 15fab208..2ad41152 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -90,9 +91,25 @@ public Contact[] getValidContacts() { return contacts.toArray(new Contact[0]); } - private void addContact(Contact contact) { + public void addContact(Contact contact) { this.addContactSilent(contact); this.save(); + this.changed(contact); + } + + public void addContacts(List contacts) { + boolean changed = false; + for (Contact c: contacts) + if (!this.getAllContacts().contains(c)) { + this.addContactSilent(c); + changed = true; + } + + if (changed) { + System.out.println("addContacts save"); + this.save(); + this.changed(contacts); + } } private void addContactSilent(Contact contact) { @@ -164,7 +181,7 @@ public void applyGroupCommand(MessageContent.GroupCommand command, Contact sende continue; } meIn |= contact.isMe(); - this.addContact(contact); + this.addContactSilent(contact); } if (!meIn) From bac9d3f061692390d2cb65ba7289962bdf16b14f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 9 Jan 2016 18:37:36 +0100 Subject: [PATCH 043/257] system: improved group control --- src/main/java/org/kontalk/system/Control.java | 40 +++++----- .../java/org/kontalk/system/GroupControl.java | 77 ++++++++++++------- .../org/kontalk/system/RosterHandler.java | 2 +- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index aa47878b..608210ca 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -49,7 +49,6 @@ import org.kontalk.model.ContactList; import org.kontalk.misc.JID; import org.kontalk.model.GroupChat; -import org.kontalk.model.GroupChat.KonGroupChat; import org.kontalk.model.GroupMetaData; import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.Attachment; @@ -86,6 +85,7 @@ public enum Status { private final AttachmentManager mAttachmentManager; private final RosterHandler mRosterHandler; private final AvatarHandler mAvatarHandler; + private final GroupControl mGroupControl; private Status mCurrentStatus = Status.DISCONNECTED; @@ -97,6 +97,7 @@ private Control(Path appDir) { mAttachmentManager = AttachmentManager.create(appDir, this); mRosterHandler = new RosterHandler(this, mClient); mAvatarHandler = new AvatarHandler(mClient); + mGroupControl = new GroupControl(this); } public RosterHandler getRosterHandler() { @@ -163,7 +164,7 @@ public boolean newInMessage(MessageIDs ids, MessageContent content) { LOGGER.info("new incoming message, "+ids); - Optional optContact = this.getOrCreateContact(ids.jid, ""); + Optional optContact = this.getOrCreateContact(ids.jid); if (!optContact.isPresent()) { LOGGER.warning("can't get contact for message"); return false; @@ -176,7 +177,7 @@ public boolean newInMessage(MessageIDs ids, Coder.decryptMessage(protoMessage); } - // note: decryption must be successful to select group chat + // NOTE: decryption must be successful to select group chat Optional optGID = protoMessage.getContent().getGroupData(); // TODO ignore message if it contains unexpected group commands @@ -203,11 +204,14 @@ public boolean newInMessage(MessageIDs ids, return false; } - if (chat instanceof GroupChat) { - this.GroupControlForChat((GroupChat) chat) - .onInMessage(newMessage.getContent(), contact); - } else if (newMessage.getContent().getGroupCommand().isPresent()) { - LOGGER.warning("group command for non-group chat"); + Optional optCom = newMessage.getContent().getGroupCommand(); + if (optCom.isPresent()) { + if (chat instanceof GroupChat) { + mGroupControl.getInstanceFor((GroupChat) chat) + .onInMessage(optCom.get(), contact); + } else { + LOGGER.warning("group command for non-group chat"); + } } this.processContent(newMessage); @@ -399,7 +403,7 @@ void maySendKeyRequest(Contact contact) { mClient.sendPublicKeyRequest(contact.getJID()); } - Optional getOrCreateContact(JID jid, String name) { + Optional getOrCreateContact(JID jid) { Optional optContact = ContactList.getInstance().get(jid); if (optContact.isPresent()) return optContact; @@ -497,13 +501,6 @@ private void removeFromRoster(JID jid) { } } - private GroupControl GroupControlForChat(GroupChat chat) { - return (chat instanceof KonGroupChat) ? - new GroupControl.KonChatControl(this, (KonGroupChat) chat) : - // TODO - null; - } - /* static */ public static ViewControl create(Path appDir) { @@ -686,19 +683,20 @@ public void createGroupChat(List contacts, String subject) { List withMe = new ArrayList<>(contacts); withMe.add(me); - // TODO static new - GroupMetaData gid = new GroupMetaData.KonGroupData(me.getJID(), org.jivesoftware.smack.util.StringUtils.randomString(8)); + // TODO + KonGroupData gData = GroupControl.newKonGroupData(me.getJID()); + //MUCData gData = GroupControl.newMUCGroupData(); GroupChat chat = ChatList.getInstance().createNew(withMe.toArray(new Contact[0]), - gid, + gData, subject); - Control.this.GroupControlForChat(chat).onCreate(contacts, subject); + mGroupControl.getInstanceFor(chat).onCreate(); } public void deleteChat(Chat chat) { if (chat instanceof GroupChat) { - boolean succ = Control.this.GroupControlForChat((GroupChat) chat).beforeDelete(); + boolean succ = mGroupControl.getInstanceFor((GroupChat) chat).beforeDelete(); if (!succ) return; } diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 454eae65..171bdaf3 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -20,12 +20,14 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.logging.Logger; import org.kontalk.misc.JID; import org.kontalk.model.Contact; +import org.kontalk.model.GroupChat; import org.kontalk.model.GroupChat.KonGroupChat; +import org.kontalk.model.GroupChat.MUCChat; import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.GroupMetaData.MUCData; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.GroupCommand; @@ -33,38 +35,53 @@ * * @author Alexander Bikadorov {@literal } */ -interface GroupControl { +final class GroupControl { + private static final Logger LOGGER = Logger.getLogger(GroupControl.class.getName()); - void onCreate(List contacts, String subject); + private final Control mControl; - void onSetSubject(String subject); + GroupControl(Control control) { + mControl = control; + } - boolean beforeDelete(); + abstract class ChatControl { - // warning: call this only once for each group command! - void onInMessage(MessageContent content, Contact sender); + protected final C mChat; - class KonChatControl implements GroupControl { - private static final Logger LOGGER = Logger.getLogger(KonChatControl.class.getName()); + private ChatControl(C chat) { + mChat = chat; + } - private final Control mControl; - private final KonGroupChat mChat; + abstract void onCreate(); - KonChatControl(Control control, KonGroupChat chat) { - mControl = control; - mChat = chat; + abstract void onSetSubject(String subject); + + abstract boolean beforeDelete(); + + // + abstract void onInMessage(GroupCommand command, Contact sender); + } + + final class KonChatControl extends ChatControl { + + private KonChatControl(KonGroupChat chat) { + super(chat); } @Override - public void onCreate(List contacts, String subject) { + void onCreate() { + Contact[] contacts = mChat.getValidContacts(); + // send create group command - List jids = new ArrayList<>(contacts.size()); + List jids = new ArrayList<>(contacts.length); for (Contact c: contacts) jids.add(c.getJID()); mControl.createAndSendMessage(mChat, MessageContent.groupCommand( - MessageContent.GroupCommand.create(jids.toArray(new JID[0]),subject) + MessageContent.GroupCommand.create( + jids.toArray(new JID[0]), + mChat.getSubject()) ) ); } @@ -86,15 +103,8 @@ public boolean beforeDelete() { } @Override - public void onInMessage(MessageContent content, Contact sender) { - Optional optCom = content.getGroupCommand(); - if (!optCom.isPresent()) { - return; - } - - // TODO ignore message if it contains unexpected group command - - GroupCommand command = optCom.get(); + public void onInMessage(GroupCommand command, Contact sender) { + // TODO ignore message if it contains unexpected group command (?) // NOTE: chat was selected/created by GID so we can be sure message // and chat GIDs match @@ -116,7 +126,7 @@ public void onInMessage(MessageContent content, Contact sender) { // TODO design problem here: we need at least the public keys, but user // might dont wanna have group members in contact list for (JID jid : command.getAdded()) { - boolean succ = mControl.getOrCreateContact(jid, "").isPresent(); + boolean succ = mControl.getOrCreateContact(jid).isPresent(); if (!succ) LOGGER.warning("can't create contact, JID: "+jid); } @@ -125,4 +135,17 @@ public void onInMessage(MessageContent content, Contact sender) { mChat.applyGroupCommand(command, sender); } } + ChatControl getInstanceFor(GroupChat chat) { + // TODO + return (chat instanceof KonGroupChat) ? + new KonChatControl((KonGroupChat) chat) : + // new MUCControl((MUCChat) chat); + null; + } + + static KonGroupData newKonGroupData(JID myJID) { + return new KonGroupData(myJID, + org.jivesoftware.smack.util.StringUtils.randomString(8)); + } + } diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index c59e564b..2a1fce12 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -117,7 +117,7 @@ public void onEntryUpdate(JID jid, } public void onSubscriptionRequest(JID jid, byte[] rawKey) { - Optional optContact = mControl.getOrCreateContact(jid, ""); + Optional optContact = mControl.getOrCreateContact(jid); if (!optContact.isPresent()) return; Contact contact = optContact.get(); From 5546c98f320139fa43aef6c474b709e996102628 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 9 Jan 2016 19:34:05 +0100 Subject: [PATCH 044/257] minor code cleanup --- src/main/java/org/kontalk/client/AcknowledgedListener.java | 1 - src/main/java/org/kontalk/client/KonMessageListener.java | 4 ++-- src/main/java/org/kontalk/crypto/PGPUtils.java | 3 +-- src/main/java/org/kontalk/misc/JID.java | 6 +++--- src/main/java/org/kontalk/model/Transmission.java | 2 +- src/main/java/org/kontalk/util/Tr.java | 4 +++- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/kontalk/client/AcknowledgedListener.java b/src/main/java/org/kontalk/client/AcknowledgedListener.java index 328fd35d..9b2f4f90 100644 --- a/src/main/java/org/kontalk/client/AcknowledgedListener.java +++ b/src/main/java/org/kontalk/client/AcknowledgedListener.java @@ -67,5 +67,4 @@ public void processPacket(Stanza p) { mControl.setSent(MessageIDs.from(m)); } - } diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 43c08654..0f180169 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -135,8 +135,6 @@ private void processChatMessage(Message m) { // note: thread and subject are null if message comes from the Kontalk // Android client - String threadID = m.getThread() != null ? m.getThread() : ""; - // TODO a message can contain all sorts of extensions, we should loop // over all of them @@ -157,6 +155,8 @@ private void processChatMessage(Message m) { optServerDate = Optional.of(date); } + String threadID = StringUtils.defaultString(m.getThread()); + // process possible chat state notification (XEP-0085) ExtensionElement csExt = m.getExtension(ChatStateExtension.NAMESPACE); ChatState chatState = null; diff --git a/src/main/java/org/kontalk/crypto/PGPUtils.java b/src/main/java/org/kontalk/crypto/PGPUtils.java index cb382c14..ff166d56 100644 --- a/src/main/java/org/kontalk/crypto/PGPUtils.java +++ b/src/main/java/org/kontalk/crypto/PGPUtils.java @@ -80,8 +80,7 @@ public final class PGPUtils { /** Singleton for converting a PGP key to a JCA key. */ private static JcaPGPKeyConverter sKeyConverter; - private PGPUtils() { - } + private PGPUtils() {} /** * A contacts public keys for encryption and signing together with UID and diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index cd4362af..694a93f0 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -24,7 +24,7 @@ import org.kontalk.system.Config; /** - * A Jabber ID (the address of an XMPP client or user). Immutable. + * A Jabber ID (the address of an XMPP entity). Immutable. * * @author Alexander Bikadorov {@literal } */ @@ -40,11 +40,11 @@ private JID(String local, String domain, String resource) { mResource = resource; } - public String local(){ + public String local() { return mLocal; } - public String domain(){ + public String domain() { return mDomain; } diff --git a/src/main/java/org/kontalk/model/Transmission.java b/src/main/java/org/kontalk/model/Transmission.java index 0614680d..a4882fa4 100644 --- a/src/main/java/org/kontalk/model/Transmission.java +++ b/src/main/java/org/kontalk/model/Transmission.java @@ -111,7 +111,7 @@ private int insert(int messageID) { int id = db.execInsert(TABLE, values); if (id <= 0) { - LOGGER.log(Level.WARNING, "db, could not insert"); + LOGGER.log(Level.WARNING, "could not insert"); return -2; } return id; diff --git a/src/main/java/org/kontalk/util/Tr.java b/src/main/java/org/kontalk/util/Tr.java index 37c4465e..111db845 100644 --- a/src/main/java/org/kontalk/util/Tr.java +++ b/src/main/java/org/kontalk/util/Tr.java @@ -55,7 +55,7 @@ public class Tr { /** * Translate string used in user interface. * Spaces at beginning or end of string not supported! - * @param s string thats wants to be translated (in English) + * @param s string that wants to be translated (in English) * @return translation of input string (depending of platform language) */ public static String tr(String s) { @@ -67,6 +67,8 @@ public static String tr(String s) { public static void init() { // get language String lang = Locale.getDefault().getLanguage(); + // for testing + //String lang = new Locale("zh").getLanguage(); if (lang.equals(DEFAULT_LANG)) { return; } From 28da2d2c6ae2b286b0098351b123a7d89a83f21b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 9 Jan 2016 19:36:10 +0100 Subject: [PATCH 045/257] client: filter MUC presences --- src/main/java/org/kontalk/client/PresenceListener.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index 5968e6aa..9dcb65b5 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -27,6 +27,7 @@ import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smackx.muc.packet.MUCUser; import org.kontalk.misc.JID; import org.kontalk.system.RosterHandler; @@ -62,6 +63,12 @@ public PresenceListener(Roster roster, RosterHandler handler) { @Override public void processPacket(Stanza packet) { + if (MUCUser.from(packet) != null) { + // handled by MUC manager + LOGGER.config("ignoring MUC presence, from: "+packet.getFrom()); + return; + } + LOGGER.config("packet: "+packet); Presence presence = (Presence) packet; From 38e3ea302aaea46717d058db317b32af1cdec44b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 10 Jan 2016 15:52:25 +0100 Subject: [PATCH 046/257] model: removed obsolete synchronized --- src/main/java/org/kontalk/model/KonMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/KonMessage.java b/src/main/java/org/kontalk/model/KonMessage.java index 1553d8e8..831731d7 100644 --- a/src/main/java/org/kontalk/model/KonMessage.java +++ b/src/main/java/org/kontalk/model/KonMessage.java @@ -258,7 +258,7 @@ public void save() { db.execUpdate(TABLE, set, mID); } - protected synchronized void changed(Object arg) { + protected void changed(Object arg) { this.setChanged(); this.notifyObservers(arg); } From 322b85dc97e7e50cabf78a90619a85b826b613ea Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 10 Jan 2016 15:59:19 +0100 Subject: [PATCH 047/257] client/control: use message IDs for chat state --- .../java/org/kontalk/client/KonMessageListener.java | 7 ++----- src/main/java/org/kontalk/system/Control.java | 10 ++++------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 0f180169..196dcade 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -155,15 +155,14 @@ private void processChatMessage(Message m) { optServerDate = Optional.of(date); } - String threadID = StringUtils.defaultString(m.getThread()); + MessageIDs ids = MessageIDs.from(m); // process possible chat state notification (XEP-0085) ExtensionElement csExt = m.getExtension(ChatStateExtension.NAMESPACE); ChatState chatState = null; if (csExt != null) { chatState = ((ChatStateExtension) csExt).getChatState(); - mControl.processChatState(JID.bare(m.getFrom()), - threadID, + mControl.processChatState(ids, optServerDate, chatState); } @@ -183,8 +182,6 @@ private void processChatMessage(Message m) { return; } - MessageIDs ids = MessageIDs.from(m); - // add message boolean success = mControl.newInMessage(ids, optServerDate, content); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 608210ca..79c955e7 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -49,7 +49,6 @@ import org.kontalk.model.ContactList; import org.kontalk.misc.JID; import org.kontalk.model.GroupChat; -import org.kontalk.model.GroupMetaData; import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; @@ -247,8 +246,7 @@ public void setMessageError(MessageIDs ids, Condition condition, String errorTex /** * Inform model (and view) about a received chat state notification. */ - public void processChatState(JID jid, - String xmppThreadID, + public void processChatState(MessageIDs ids, Optional serverDate, ChatState chatState) { if (serverDate.isPresent()) { @@ -258,14 +256,14 @@ public void processChatState(JID jid, return; } } - Optional optContact = ContactList.getInstance().get(jid); + Optional optContact = ContactList.getInstance().get(ids.jid); if (!optContact.isPresent()) { - LOGGER.info("can't find contact with jid: "+jid); + LOGGER.info("can't find contact with jid: "+ids.jid); return; } Contact contact = optContact.get(); // TODO chat states for group chats? - Optional optChat = ChatList.getInstance().get(contact, xmppThreadID); + Optional optChat = ChatList.getInstance().get(contact, ids.xmppThreadID); if (!optChat.isPresent()) return; From 94cda36a5fd35a9986809316c31f929d98e56954 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 10 Jan 2016 16:22:16 +0100 Subject: [PATCH 048/257] fix ID JID for sent message acks --- .../org/kontalk/client/AcknowledgedListener.java | 4 ++-- src/main/java/org/kontalk/system/Control.java | 2 +- src/main/java/org/kontalk/util/ClientUtils.java | 15 +++++++++++---- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/kontalk/client/AcknowledgedListener.java b/src/main/java/org/kontalk/client/AcknowledgedListener.java index 9b2f4f90..84d9ac00 100644 --- a/src/main/java/org/kontalk/client/AcknowledgedListener.java +++ b/src/main/java/org/kontalk/client/AcknowledgedListener.java @@ -43,7 +43,7 @@ public AcknowledgedListener(Control control) { @Override public void processPacket(Stanza p) { - // note: the packet is not the acknowledgement itself but the packet that + // NOTE: the packet is not the acknowledgement itself but the packet that // is acknowledged if (!(p instanceof Message)) { // we are only interested in acks for messages @@ -65,6 +65,6 @@ public void processPacket(Stanza p) { return; } - mControl.setSent(MessageIDs.from(m)); + mControl.messageSent(MessageIDs.to(m)); } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 79c955e7..f8073b5c 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -220,7 +220,7 @@ public boolean newInMessage(MessageIDs ids, return newMessage.getID() >= -1; } - public void setSent(MessageIDs ids) { + public void messageSent(MessageIDs ids) { Optional optMessage = findMessage(ids); if (!optMessage.isPresent()) return; diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 8697c158..779b4c0f 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -44,13 +44,13 @@ public final class ClientUtils { private static final Logger LOGGER = Logger.getLogger(ClientUtils.class.getName()); /** - * Message attributes to identify the chat for a message. + * Message attributes for identifying the chat for a message. + * KonGroupData is missing here as this could be part of the encrypted content. */ public static class MessageIDs { public final JID jid; public final String xmppID; public final String xmppThreadID; - //public final Optional groupID; private MessageIDs(JID jid, String xmppID, String threadID) { this.jid = jid; @@ -63,9 +63,16 @@ public static MessageIDs from(Message m) { } public static MessageIDs from(Message m, String receiptID) { + return create(m, m.getFrom(), receiptID); + } + + public static MessageIDs to(Message m) { + return create(m, m.getTo(), ""); + } + + private static MessageIDs create(Message m, String jid, String receiptID) { return new MessageIDs( - // TODO - JID.full(StringUtils.defaultString(m.getFrom())), + JID.full(StringUtils.defaultString(jid)), !receiptID.isEmpty() ? receiptID : StringUtils.defaultString(m.getStanzaId()), StringUtils.defaultString(m.getThread())); From 01c761757cd03f340131672e6d6111ce94daf282 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 10 Jan 2016 17:35:04 +0100 Subject: [PATCH 049/257] fixup client common extension deps --- client-common-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common-java b/client-common-java index 01fb70a8..52f5e57f 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit 01fb70a8d0843cf7e67996a648f42a5ded0c401e +Subproject commit 52f5e57f6249ba9428cc83023071fe58d879259d From 4a470f48a9570a1d809864249733663879e466e3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 10 Jan 2016 19:23:24 +0100 Subject: [PATCH 050/257] static app dir --- src/main/java/org/kontalk/Kontalk.java | 26 +++++++++---------- src/main/java/org/kontalk/client/Client.java | 7 +++-- src/main/java/org/kontalk/model/Account.java | 14 +++------- .../org/kontalk/system/AttachmentManager.java | 9 ++++--- src/main/java/org/kontalk/system/Config.java | 17 +++++------- src/main/java/org/kontalk/system/Control.java | 9 +++---- .../java/org/kontalk/system/Database.java | 7 ++--- 7 files changed, 38 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 2ed86321..23c6902e 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -36,8 +36,6 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.model.ChatList; import org.kontalk.model.ContactList; -import org.kontalk.model.Account; -import org.kontalk.system.Config; import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; import org.kontalk.util.CryptoUtils; @@ -51,9 +49,9 @@ public final class Kontalk { private static final Logger LOGGER = Logger.getLogger(Kontalk.class.getName()); public static final String VERSION = "3.0.4"; - private final Path mAppDir; private static ServerSocket RUN_LOCK = null; + private static Path APP_DIR = null; Kontalk() { // platform dependent configuration directory @@ -62,7 +60,7 @@ public final class Kontalk { } Kontalk(Path appDir) { - mAppDir = appDir; + APP_DIR = appDir; } private void start() { @@ -89,7 +87,7 @@ private void start() { } // create app directory - boolean created = mAppDir.toFile().mkdirs(); + boolean created = APP_DIR.toFile().mkdirs(); if (created) LOGGER.info("created application directory"); @@ -100,7 +98,7 @@ private void start() { if (h instanceof ConsoleHandler) h.setLevel(Level.CONFIG); } - String logPath = mAppDir.resolve("debug.log").toString(); + String logPath = APP_DIR.resolve("debug.log").toString(); Handler fileHandler = null; try { fileHandler = new FileHandler(logPath, 1024*1000, 1, true); @@ -121,11 +119,7 @@ private void start() { // register provider PGPUtils.registerProvider(); - - Config.initialize(mAppDir.resolve(Config.FILENAME)); - Account.initialize(mAppDir); - - ViewControl control = Control.create(mAppDir); + ViewControl control = Control.create(); Optional optView = View.create(control); if (!optView.isPresent()) { @@ -135,7 +129,8 @@ private void start() { View view = optView.get(); try { - Database.initialize(mAppDir.resolve(Database.FILENAME)); + // do now to test if successful + Database.initialize(); } catch (KonException ex) { LOGGER.log(Level.SEVERE, "can't initialize database", ex); control.shutDown(); @@ -151,8 +146,11 @@ private void start() { control.launch(); } - public Path getAppDir() { - return mAppDir; + public static Path appDir() { + if (APP_DIR == null) + throw new IllegalStateException("app dir not initialized"); + + return APP_DIR; } public static void exit() { diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index ced7fec4..69b40456 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -45,6 +44,7 @@ import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; +import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; import org.kontalk.crypto.PersonalKey; @@ -83,10 +83,9 @@ public Client(Control control) { // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; - } - public static void setCapsCache(Path appDir) { - File cacheDir = appDir.resolve(CAPS_CACHE_DIR).toFile(); + // setting caps cache + File cacheDir = Kontalk.appDir().resolve(CAPS_CACHE_DIR).toFile(); if (cacheDir.mkdir()) LOGGER.info("created caps cache directory"); diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index 1d29dd2b..a237fc9f 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -35,6 +35,7 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.jivesoftware.smack.util.StringUtils; +import org.kontalk.Kontalk; import org.kontalk.misc.KonException; import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; @@ -190,17 +191,10 @@ private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } - public synchronized static void initialize(Path keyDir) { - if (INSTANCE != null) { - LOGGER.warning("account loader already initialized"); - return; - } - INSTANCE = new Account(keyDir, Config.getInstance()); - } - public synchronized static Account getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("account loader not initialized"); + if (INSTANCE == null) { + INSTANCE = new Account(Kontalk.appDir(), Config.getInstance()); + } return INSTANCE; } } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 39211a31..87d34144 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -35,6 +35,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; +import org.kontalk.Kontalk; import org.kontalk.client.HTTPFileClient; import org.kontalk.crypto.Coder; import org.kontalk.crypto.Coder.Encryption; @@ -344,12 +345,12 @@ public static boolean isImage(String mimeType) { return mimeType.startsWith("image"); } - static AttachmentManager create(Path baseDir, Control control) { - AttachmentManager downloader = new AttachmentManager(baseDir, control); + static AttachmentManager create(Control control) { + AttachmentManager manager = new AttachmentManager(Kontalk.appDir(), control); - new Thread(downloader).start(); + new Thread(manager).start(); - return downloader; + return manager; } /** diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index 97bad67a..91165104 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -26,6 +26,7 @@ import java.util.logging.Logger; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; +import org.kontalk.Kontalk; import org.kontalk.util.Tr; /** @@ -39,7 +40,8 @@ public final class Config extends PropertiesConfiguration { private static Config INSTANCE = null; - public static final String FILENAME = "kontalk.properties"; + private static final String FILENAME = "kontalk.properties"; + // all configuration property keys // disable network property for now -> same as server host //public static final String SERV_NET = "server.network"; @@ -118,17 +120,10 @@ public void saveToFile() { } } - public synchronized static void initialize(Path configFile) { - if (INSTANCE != null) { - LOGGER.warning("configuration already initialized"); - return; - } - INSTANCE = new Config(configFile); - } - public synchronized static Config getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("configuration not initialized"); + if (INSTANCE == null) { + INSTANCE = new Config(Kontalk.appDir().resolve(Config.FILENAME)); + } return INSTANCE; } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index f8073b5c..07f59839 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -88,12 +88,12 @@ public enum Status { private Status mCurrentStatus = Status.DISCONNECTED; - private Control(Path appDir) { + private Control() { mViewControl = new ViewControl(); mClient = new Client(this); mChatStateManager = new ChatStateManager(mClient); - mAttachmentManager = AttachmentManager.create(appDir, this); + mAttachmentManager = AttachmentManager.create(this); mRosterHandler = new RosterHandler(this, mClient); mAvatarHandler = new AvatarHandler(mClient); mGroupControl = new GroupControl(this); @@ -501,9 +501,8 @@ private void removeFromRoster(JID jid) { /* static */ - public static ViewControl create(Path appDir) { - Client.setCapsCache(appDir); - return new Control(appDir).mViewControl; + public static ViewControl create() { + return new Control().mViewControl; } private static Optional findMessage(MessageIDs ids) { diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 31faf86c..733b4c54 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -35,6 +35,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; +import org.kontalk.Kontalk; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; import org.kontalk.model.KonMessage; @@ -59,9 +60,9 @@ public final class Database { private static Database INSTANCE = null; - public static final String FILENAME = "kontalk_db.sqlite"; public static final String SQL_ID = "_id INTEGER PRIMARY KEY AUTOINCREMENT, "; + private static final String FILENAME = "kontalk_db.sqlite"; private static final int DB_VERSION = 3; private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS "; private static final String SV = "schema_version"; @@ -356,8 +357,8 @@ public static String setString(String s) { return s.isEmpty() ? null : s; } - public static void initialize(Path dbFile) throws KonException { - INSTANCE = new Database(dbFile); + public static void initialize() throws KonException { + INSTANCE = new Database(Kontalk.appDir().resolve(Database.FILENAME)); } public static Database getInstance() { From 84fd26fa75f1b991b42ebefdb7a14bb64b072bd4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 11 Jan 2016 14:27:49 +0100 Subject: [PATCH 051/257] utils: extended functions to read images --- .../org/kontalk/system/AttachmentManager.java | 2 +- .../java/org/kontalk/util/MediaUtils.java | 29 ++++++++++++++----- .../java/org/kontalk/view/ComponentUtils.java | 5 ++-- .../java/org/kontalk/view/ImageLoader.java | 7 +++-- .../java/org/kontalk/view/MessageList.java | 7 +++-- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 87d34144..cfe9f591 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -262,7 +262,7 @@ boolean createImagePreview(KonMessage message) { if (!isImage(att.getMimeType())) return false; - BufferedImage image = MediaUtils.readImage(path.toString()); + BufferedImage image = MediaUtils.readImage(path); if (image.getWidth() <= THUMBNAIL_DIM.width && image.getHeight() <= THUMBNAIL_DIM.height) return false; diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index b2387d21..1f0642db 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -81,16 +82,20 @@ private static void play(String fileName) { mAudioClip.play(); } - public static BufferedImage readImage(String path) { + public static BufferedImage readImage(Path path) { + Optional optImg = readImage(path.toFile()); + return optImg.isPresent() ? + optImg.get() : + new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB); + } + + public static Optional readImage(File file) { try { - BufferedImage image = ImageIO.read(new File(path)); - if (image != null) { - return image; - } + return Optional.ofNullable(ImageIO.read(file)); } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't read image, path: "+path, ex); + LOGGER.log(Level.WARNING, "can't read image, path: "+file.getPath(), ex); } - return new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB); + return Optional.empty(); } public static Optional readImage(byte[] imgData) { @@ -102,6 +107,16 @@ public static Optional readImage(byte[] imgData) { return Optional.empty(); } + public static boolean writeImage(BufferedImage img, String format, File output) { + try { + ImageIO.write(img, format, output); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't save avatar", ex); + return false; + } + return true; + } + public static byte[] imageToByteArray(Image image, String format) { BufferedImage bufImage = new BufferedImage( image.getWidth(null), image.getHeight(null), diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index b5fa047c..5e5a2c9f 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -57,6 +57,7 @@ import java.awt.event.WindowEvent; import java.awt.event.WindowStateListener; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -792,7 +793,7 @@ static class AttachmentPanel extends GroupPanel { private final WebLabel mStatus; private final WebLinkLabel mAttLabel; - private String mImagePath = ""; + private Path mImagePath = Paths.get(""); AttachmentPanel() { super(View.GAP_SMALL, false); @@ -804,7 +805,7 @@ static class AttachmentPanel extends GroupPanel { this.add(mAttLabel); } - void setImage(String path) { + void setImage(Path path) { if (path.equals(mImagePath)) return; diff --git a/src/main/java/org/kontalk/view/ImageLoader.java b/src/main/java/org/kontalk/view/ImageLoader.java index da4ef8b6..63381cce 100644 --- a/src/main/java/org/kontalk/view/ImageLoader.java +++ b/src/main/java/org/kontalk/view/ImageLoader.java @@ -22,6 +22,7 @@ import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; +import java.nio.file.Path; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; import org.kontalk.system.AttachmentManager; @@ -36,7 +37,7 @@ class ImageLoader { private ImageLoader() {} // TODO Swing + async == a damn mess - static void setImageIconAsync(WebLinkLabel view, String path) { + static void setImageIconAsync(WebLinkLabel view, Path path) { AsyncLoader run = new AsyncLoader(view, path); // TODO all at once? queue not that good either //new Chat(run).start(); @@ -46,9 +47,9 @@ static void setImageIconAsync(WebLinkLabel view, String path) { private static final class AsyncLoader implements Runnable, ImageObserver { private final WebLinkLabel view; - private final String path; + private final Path path; - AsyncLoader(WebLinkLabel view, String path) { + AsyncLoader(WebLinkLabel view, Path path) { this.view = view; this.path = path; } diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 41e25005..baf96a7e 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -36,6 +36,7 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Date; import java.util.HashSet; import java.util.Optional; @@ -555,13 +556,13 @@ private void updateAttachment() { // image thumbnail preview Optional optImagePath = mView.getControl().getImagePath(mValue); - String imagePath = optImagePath.isPresent() ? optImagePath.get().toString() : ""; - mAttPanel.setImage(imagePath); + Path image = optImagePath.isPresent() ? optImagePath.get() : Paths.get(""); + mAttPanel.setImage(image); // link to the file Path linkPath = mView.getControl().getFilePath(att); if (!linkPath.toString().isEmpty()) { - mAttPanel.setLink(imagePath.isEmpty() ? + mAttPanel.setLink(image.toString().isEmpty() ? linkPath.getFileName().toString() : "", linkPath); From a97269a913105328e6bf44877eaef0be7745fcf4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 12 Jan 2016 15:05:54 +0100 Subject: [PATCH 052/257] improved contact avatar, saved as file now --- src/main/java/org/kontalk/model/Contact.java | 60 +++++++++- .../org/kontalk/system/AvatarHandler.java | 29 ++--- .../java/org/kontalk/util/MediaUtils.java | 10 +- .../java/org/kontalk/view/AvatarLoader.java | 112 +++++++++--------- 4 files changed, 127 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 1f715c12..a1a0abac 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -18,6 +18,8 @@ package org.kontalk.model; +import java.awt.image.BufferedImage; +import java.io.File; import java.sql.ResultSet; import java.sql.SQLException; import org.kontalk.misc.JID; @@ -32,8 +34,10 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Presence; +import org.kontalk.Kontalk; import org.kontalk.system.Database; import org.kontalk.util.EncodingUtils; +import org.kontalk.util.MediaUtils; import org.kontalk.util.XMPPUtils; /** @@ -263,10 +267,15 @@ public Optional getAvatar() { } public void setAvatar(Avatar avatar) { + // delete old + if (mAvatar != null) + mAvatar.delete(); + + // set new mAvatar = avatar; - // TODO - //this.save(); - this.changed(mAvatar); + this.save(); + + this.changed(avatar); } public boolean isMe() { @@ -289,6 +298,7 @@ void setDeleted() { mEncrypted = false; mKey = ""; mFingerprint = ""; + mAvatar.delete(); mAvatar = null; this.save(); @@ -339,13 +349,45 @@ static Contact load(ResultSet rs) throws SQLException { return new Contact(id, jid, name, status, lastSeen, encr, key, fp); } + /** + * Contact avatar image. Immutable. + */ public static class Avatar { + + private static final String AVATAR_FORMAT = "png"; + private static final String AVATAR_DIR = "avatars"; + + /** SHA1 hash of image data. */ public final String id; - public final byte[] data; - public Avatar(String id, byte[] data) { + private BufferedImage image = null; + + public Avatar(String id, BufferedImage image) { this.id = id; - this.data = data; + this.image = image; + + // save + boolean succ = MediaUtils.writeImage(this.image, AVATAR_FORMAT, this.file()); + if (!succ) + LOGGER.warning("can't save avatar image: "+this.id); + } + + public Optional loadImage() { + if (image == null) + image = MediaUtils.readImage(this.file()).orElse(null); + + return Optional.ofNullable(image); + } + + void delete() { + boolean succ = this.file().delete(); + if (succ) + LOGGER.warning("could not delete avatar file: "+this.id); + } + + private File file() { + return Kontalk.appDir().resolve(AVATAR_DIR) + .resolve(this.id + "." + AVATAR_FORMAT).toFile(); } @Override @@ -364,5 +406,11 @@ public int hashCode() { hash = 59 * hash + Objects.hashCode(this.id); return hash; } + + public static void createDir() { + boolean created = Kontalk.appDir().resolve(AVATAR_DIR).toFile().mkdir(); + if (created) + LOGGER.info("created avatar directory"); + } } } diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 5b117317..bda3e207 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -18,7 +18,6 @@ package org.kontalk.system; -import java.awt.Image; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.List; @@ -27,7 +26,6 @@ import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; import org.kontalk.client.Client; -import org.kontalk.misc.Callback; import org.kontalk.misc.JID; import org.kontalk.model.Contact; import org.kontalk.model.Contact.Avatar; @@ -43,13 +41,14 @@ public final class AvatarHandler { public static final List SUPPORTED_TYPES = Arrays.asList(ImageIO.getReaderMIMETypes()); - private static final int MAX_SIZE = 40; - private static final String STORE_FORMAT = "jpg"; + private static final int MAX_SIZE = 1024 * 250; private final Client mClient; AvatarHandler(Client client) { mClient = client; + + Contact.Avatar.createDir(); } public void onNotify(JID jid, String id) { @@ -66,17 +65,19 @@ public void onNotify(JID jid, String id) { } Optional optAvatar = contact.getAvatar(); - if (optAvatar.isPresent() && - id.equals(DigestUtils.sha1Hex(optAvatar.get().id))) + if (optAvatar.isPresent() && optAvatar.get().id.equals(id)) // avatar is not new return; mClient.requestAvatar(jid, id); } - public void onData(JID jid, final String id, byte[] avatarData) { + public void onData(JID jid, String id, byte[] avatarData) { LOGGER.info("new avatar, jid: "+jid+" id: "+id); + if (avatarData.length > MAX_SIZE) + LOGGER.info("avatar data too long: "+avatarData.length); + final Optional optContact = ContactList.getInstance().get(jid); if (!optContact.isPresent()) { LOGGER.warning("can't find contact with jid:" + jid); @@ -92,18 +93,6 @@ public void onData(JID jid, final String id, byte[] avatarData) { if (!optImg.isPresent()) return; - MediaUtils.scale(optImg.get(), MAX_SIZE, MAX_SIZE, true, - new Callback.Handler(){ - @Override - public void handle(Callback callback) { - byte[] data = MediaUtils.imageToByteArray(callback.value, STORE_FORMAT); - if (data.length == 0) - return; - - Contact contact = optContact.get(); - contact.setAvatar(new Avatar(id, data)); - } - }); + optContact.get().setAvatar(new Avatar(id, optImg.get())); } - } diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 1f0642db..73c1f259 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -90,6 +90,11 @@ public static BufferedImage readImage(Path path) { } public static Optional readImage(File file) { + if (!file.exists()) { + LOGGER.warning("image file does not exist: "+file); + return Optional.empty(); + } + try { return Optional.ofNullable(ImageIO.read(file)); } catch (IOException ex) { @@ -109,12 +114,11 @@ public static Optional readImage(byte[] imgData) { public static boolean writeImage(BufferedImage img, String format, File output) { try { - ImageIO.write(img, format, output); + return ImageIO.write(img, format, output); } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't save avatar", ex); + LOGGER.log(Level.WARNING, "can't save image", ex); return false; } - return true; } public static byte[] imageToByteArray(Image image, String format) { diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 0816b924..d5a1405a 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -18,14 +18,13 @@ package org.kontalk.view; -import com.alee.utils.ImageUtils; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; +import java.awt.Image; import java.awt.RenderingHints; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -34,6 +33,7 @@ import org.kontalk.model.Chat; import org.kontalk.model.Contact; import org.kontalk.model.Contact.Avatar; +import org.kontalk.util.MediaUtils; /** * Static functions for loading avatar pictures. @@ -61,64 +61,15 @@ static ImageIcon load(Contact contact) { private static ImageIcon load(Item item) { if (!CACHE.containsKey(item)) { - ImageIcon icon = null; - - if (item.avatar != null) - icon = ImageUtils.loadImage(new ByteArrayInputStream(item.avatar.data)); - - if (icon == null) - icon = new ImageIcon(fallback(item.label, item.colorCode, IMG_SIZE)); - - CACHE.put(item, icon); + CACHE.put(item, new ImageIcon(item.createImage())); } return CACHE.get(item); } - static BufferedImage createFallback(int size) { - return fallback("", 0, size); - } - - private static BufferedImage fallback(String text, int colorCode, int size) { - BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); - - // color - Color color; - if (!text.isEmpty()) { - int hue = Math.abs(colorCode) % 360; - color = Color.getHSBColor(hue / 360.0f, 1, 1); - } else { - color = FALLBACK_COLOR; - } - - Graphics2D graphics = img.createGraphics(); - graphics.setColor(color); - graphics.fillRect(0, 0, size, size); - - // letter - String letter = text.length() > 1 ? - text.substring(0, 1).toUpperCase() : - FALLBACK_LETTER; - - graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, size)); - graphics.setColor(LETTER_COLOR); - graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - FontMetrics fm = graphics.getFontMetrics(); - int w = fm.stringWidth(letter); - int h = fm.getHeight(); - int d = fm.getDescent(); - graphics.drawString(letter, - (size / 2.0f) - (w / 2.0f), - // adjust to font baseline - (size / 2.0f) + (h / 2.0f) - d); - - return img; - } - private static class Item { - final Avatar avatar; - final String label; - final int colorCode; + private final Avatar avatar; + private final String label; + private final int colorCode; Item(Contact contact) { avatar = contact.getAvatar().orElse(null); @@ -146,6 +97,16 @@ private static class Item { colorCode = hash(chat.getID()); } + Image createImage() { + if (avatar != null) { + BufferedImage img = avatar.loadImage().orElse(null); + if (img != null) + return MediaUtils.scaleAsync(img, IMG_SIZE, IMG_SIZE, true); + } + + return fallback(label, colorCode, IMG_SIZE); + } + @Override public boolean equals(Object o) { if (o == this) @@ -176,4 +137,45 @@ private static int hash(int x) { x = ((x >> 16) ^ x); return x; } + + static BufferedImage createFallback(int size) { + return fallback("", 0, size); + } + + private static BufferedImage fallback(String text, int colorCode, int size) { + BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); + + // color + Color color; + if (!text.isEmpty()) { + int hue = Math.abs(colorCode) % 360; + color = Color.getHSBColor(hue / 360.0f, 1, 1); + } else { + color = FALLBACK_COLOR; + } + + Graphics2D graphics = img.createGraphics(); + graphics.setColor(color); + graphics.fillRect(0, 0, size, size); + + // letter + String letter = text.length() > 1 ? + text.substring(0, 1).toUpperCase() : + FALLBACK_LETTER; + + graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, size)); + graphics.setColor(LETTER_COLOR); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + FontMetrics fm = graphics.getFontMetrics(); + int w = fm.stringWidth(letter); + int h = fm.getHeight(); + int d = fm.getDescent(); + graphics.drawString(letter, + (size / 2.0f) - (w / 2.0f), + // adjust to font baseline + (size / 2.0f) + (h / 2.0f) - d); + + return img; + } } From eb07cce47828eb8e8c097d977a5a090b5bd3fbba Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 12 Jan 2016 15:55:01 +0100 Subject: [PATCH 053/257] database update: added contact avatar ID --- src/main/java/org/kontalk/model/Contact.java | 19 ++++++++++++++++--- .../java/org/kontalk/system/Database.java | 7 ++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index a1a0abac..cd839a60 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -69,6 +69,7 @@ public static enum Subscription { public static final String COL_ENCR = "encrypted"; public static final String COL_PUB_KEY = "public_key"; public static final String COL_KEY_FP = "key_fingerprint"; + public static final String COL_AVATAR_ID = "avatar_id"; public static final String SCHEMA = "(" + Database.SQL_ID + COL_JID + " TEXT NOT NULL UNIQUE, " + @@ -78,7 +79,8 @@ public static enum Subscription { // boolean, send messages encrypted? COL_ENCR + " INTEGER NOT NULL, " + COL_PUB_KEY + " TEXT UNIQUE, " + - COL_KEY_FP + " TEXT UNIQUE" + + COL_KEY_FP + " TEXT UNIQUE," + + COL_AVATAR_ID + " TEXT" + ")"; private final int mID; @@ -108,6 +110,7 @@ public static enum Subscription { values.add(mEncrypted); values.add(null); // key values.add(null); // fingerprint + values.add(null); // avatar id mID = db.execInsert(TABLE, values); if (mID < 1) LOGGER.log(Level.WARNING, "could not insert contact"); @@ -121,7 +124,8 @@ public static enum Subscription { Optional lastSeen, boolean encrypted, String publicKey, - String fingerprint) { + String fingerprint, + String avatarID) { mID = id; mJID = jid; mName = name; @@ -130,6 +134,7 @@ public static enum Subscription { mEncrypted = encrypted; mKey = publicKey; mFingerprint = fingerprint.toLowerCase(); + mAvatar = avatarID.isEmpty() ? null : new Avatar(avatarID); } public JID getJID() { @@ -319,6 +324,7 @@ private void save() { set.put(COL_ENCR, mEncrypted); set.put(COL_PUB_KEY, Database.setString(mKey)); set.put(COL_KEY_FP, Database.setString(mFingerprint)); + set.put(COL_AVATAR_ID, Database.setString(mAvatar != null ? mAvatar.id : "")); db.execUpdate(TABLE, set, mID); } @@ -346,7 +352,9 @@ static Contact load(ResultSet rs) throws SQLException { boolean encr = rs.getBoolean(Contact.COL_ENCR); String key = Database.getString(rs, Contact.COL_PUB_KEY); String fp = Database.getString(rs, Contact.COL_KEY_FP); - return new Contact(id, jid, name, status, lastSeen, encr, key, fp); + String avatarID = Database.getString(rs, Contact.COL_AVATAR_ID); + + return new Contact(id, jid, name, status, lastSeen, encr, key, fp, avatarID); } /** @@ -372,6 +380,11 @@ public Avatar(String id, BufferedImage image) { LOGGER.warning("can't save avatar image: "+this.id); } + // used when loading from database + Avatar(String id) { + this.id = id; + } + public Optional loadImage() { if (image == null) image = MediaUtils.readImage(this.file()).orElse(null); diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 733b4c54..3f86c667 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -63,7 +63,7 @@ public final class Database { public static final String SQL_ID = "_id INTEGER PRIMARY KEY AUTOINCREMENT, "; private static final String FILENAME = "kontalk_db.sqlite"; - private static final int DB_VERSION = 3; + private static final int DB_VERSION = 4; private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS "; private static final String SV = "schema_version"; private static final String UV = "user_version"; @@ -176,6 +176,11 @@ private void update(int fromVersion) throws SQLException { mConn.createStatement().execute("ALTER TABLE "+Chat.TABLE+ " ADD COLUMN "+Chat.COL_GD+" DEFAULT NULL"); } + if (fromVersion < 4) { + mConn.createStatement().execute("ALTER TABLE "+Contact.TABLE+ + " ADD COLUMN "+Contact.COL_AVATAR_ID+" DEFAULT NULL"); + } + // set new version mConn.createStatement().execute("PRAGMA "+UV+" = "+DB_VERSION); From c537c3ab1d9d20ae9d87c184746ca7e54e93c38b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 12 Jan 2016 16:00:19 +0100 Subject: [PATCH 054/257] client: request avatar from other users (XEP-0084) --- src/main/java/org/kontalk/client/Client.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 69b40456..d75e3fc1 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -44,6 +44,7 @@ import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; @@ -71,9 +72,9 @@ private static enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; - private AvatarSendReceiver mAvatarSendReceiver; private KonConnection mConn = null; + private AvatarSendReceiver mAvatarSendReceiver = null; public Client(Control control) { mControl = control; @@ -118,8 +119,6 @@ public void connect(PersonalKey key) { key.getBridgeCertificate(), validateCertificate); - mAvatarSendReceiver = new AvatarSendReceiver(mConn, mControl.getAvatarHandler()); - // connection listener mConn.addConnectionListener(new KonConnectionListener(mControl)); @@ -127,9 +126,11 @@ public void connect(PersonalKey key) { // subscriptions handled by roster handler roster.setSubscriptionMode(Roster.SubscriptionMode.manual); + mAvatarSendReceiver = new AvatarSendReceiver(mConn, mControl.getAvatarHandler()); + // packet listeners - RosterHandler rosterSyncer = mControl.getRosterHandler(); - RosterListener rl = new KonRosterListener(roster, rosterSyncer); + RosterHandler rosterHandler = mControl.getRosterHandler(); + RosterListener rl = new KonRosterListener(roster, rosterHandler); roster.addRosterListener(rl); StanzaFilter messageFilter = new StanzaTypeFilter(Message.class); @@ -147,7 +148,11 @@ public void connect(PersonalKey key) { mConn.addAsyncStanzaListener(new PublicKeyListener(mControl), publicKeyFilter); StanzaFilter presenceFilter = new StanzaTypeFilter(Presence.class); - mConn.addAsyncStanzaListener(new PresenceListener(roster, rosterSyncer), presenceFilter); + mConn.addAsyncStanzaListener(new PresenceListener(roster, rosterHandler), presenceFilter); + + // our service discovery: want avatar from other users + ServiceDiscoveryManager.getInstanceFor(mConn). + addFeature(AvatarSendReceiver.NOTIFY_FEATURE); // listen to all IQ errors mConn.addAsyncStanzaListener(this, IQTypeFilter.ERROR); From c3595280d8acfbf3ffee303b85b44518d702b72c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 12 Jan 2016 16:08:04 +0100 Subject: [PATCH 055/257] view: show contact avatar in chat view --- src/main/java/org/kontalk/client/AvatarSendReceiver.java | 7 +++++-- src/main/java/org/kontalk/view/ChatView.java | 8 ++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index 50b1eeab..a944d2b1 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -37,6 +37,10 @@ import org.kontalk.system.AvatarHandler; /** + * Manage publishing and requesting user avatars (XEP-0084). + * + * Metadata notification events are incoming as PubSub messages from message + * listener. * * @author Alexander Bikadorov {@literal } */ @@ -71,7 +75,6 @@ final class AvatarSendReceiver { // TODO beta.kontalk.net does not support this void publish(String id, byte[] data) { - // TODO } void processMetadataEvent(JID jid, ItemsExtension itemsExt) { @@ -113,7 +116,7 @@ void processMetadataEvent(JID jid, ItemsExtension itemsExt) { } void requestAndListen(final JID jid, final String id) { - // I really dont get how to use this + // I dont get how to use this here //PubSubManager manager = new PubSubManager(conn); PubSub request = new PubSub(jid.toBare().string(), diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index b31acc64..88ccd107 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -89,6 +89,7 @@ final class ChatView extends WebPanel implements Observer { private final View mView; + private final WebLabel mAvatarLabel; private final WebLabel mTitleLabel; private final WebLabel mSubTitleLabel; private final WebScrollPane mScrollPane; @@ -110,6 +111,10 @@ final class ChatView extends WebPanel implements Observer { WebPanel titlePanel = new WebPanel(false, new BorderLayout(View.GAP_SMALL, View.GAP_SMALL)); titlePanel.setMargin(View.MARGIN_DEFAULT); + + mAvatarLabel = new WebLabel(); + titlePanel.add(mAvatarLabel, BorderLayout.WEST); + mTitleLabel = new WebLabel(); mTitleLabel.setFontSize(16); mTitleLabel.setDrawShade(true); @@ -409,6 +414,9 @@ private void onChatChange() { Chat chat = optChat.get(); // update if chat changes... + // avatar + mAvatarLabel.setIcon(AvatarLoader.load(chat)); + // chat titles mTitleLabel.setText(Utils.chatTitle(chat)); List contacts = Utils.contactList(chat); From 774a3fa3ba2dca0b80fd87c0125714a2634f1b35 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 12 Jan 2016 18:25:24 +0100 Subject: [PATCH 056/257] reduced occurence of overused Optionals --- src/main/java/org/kontalk/Kontalk.java | 6 +- .../org/kontalk/client/KonMessageSender.java | 32 +++--- src/main/java/org/kontalk/crypto/Coder.java | 17 ++- .../java/org/kontalk/crypto/Decryptor.java | 13 +-- .../java/org/kontalk/crypto/Encryptor.java | 11 +- src/main/java/org/kontalk/model/Chat.java | 14 +-- src/main/java/org/kontalk/model/ChatList.java | 12 +- src/main/java/org/kontalk/model/Contact.java | 6 +- .../java/org/kontalk/model/ContactList.java | 12 +- .../java/org/kontalk/model/GroupChat.java | 19 ++-- .../java/org/kontalk/model/InMessage.java | 8 +- .../java/org/kontalk/model/KonMessage.java | 6 +- .../java/org/kontalk/model/Transmission.java | 6 +- .../org/kontalk/system/AttachmentManager.java | 43 +++----- .../org/kontalk/system/AvatarHandler.java | 20 ++-- src/main/java/org/kontalk/system/Control.java | 104 +++++++++--------- .../java/org/kontalk/system/Database.java | 3 +- .../org/kontalk/system/RosterHandler.java | 73 ++++++------ .../java/org/kontalk/util/MediaUtils.java | 6 +- .../java/org/kontalk/view/ChatDetails.java | 9 +- .../java/org/kontalk/view/ChatListView.java | 31 +++--- src/main/java/org/kontalk/view/ChatView.java | 58 +++++----- .../org/kontalk/view/ConfigurationDialog.java | 12 +- .../org/kontalk/view/ContactListView.java | 13 +-- .../java/org/kontalk/view/ImportDialog.java | 7 +- .../java/org/kontalk/view/MessageList.java | 21 ++-- src/main/java/org/kontalk/view/View.java | 12 +- 27 files changed, 261 insertions(+), 313 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 23c6902e..3280312f 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -25,7 +25,6 @@ import java.net.ServerSocket; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Optional; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; import java.util.logging.Handler; @@ -121,12 +120,11 @@ private void start() { ViewControl control = Control.create(); - Optional optView = View.create(control); - if (!optView.isPresent()) { + View view = View.create(control).orElse(null); + if (view == null) { control.shutDown(); return; // never reached } - View view = optView.get(); try { // do now to test if successful diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 7db75135..3029d460 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -19,7 +19,6 @@ package org.kontalk.client; import java.util.ArrayList; -import java.util.Optional; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.chatstates.ChatState; @@ -65,8 +64,8 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { } MessageContent content = message.getContent(); - Optional optAtt = content.getAttachment(); - if (optAtt.isPresent() && !optAtt.get().hasURL()) { + MessageContent.Attachment att = content.getAttachment().orElse(null); + if (att != null && !att.hasURL()) { LOGGER.warning("attachment not uploaded"); message.setStatus(KonMessage.Status.ERROR); return false; @@ -96,19 +95,19 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { protoMessage.addExtension(new ChatStateExtension(ChatState.active)); if (encrypted) { - Optional encryptedData = content.isComplex() || chat.isGroupChat() ? + byte[] encryptedData = content.isComplex() || chat.isGroupChat() ? Coder.encryptStanza(message, - rawMessage(content, chat, true).toXML().toString()) : - Coder.encryptMessage(message); + rawMessage(content, chat, true).toXML().toString()).orElse(null) : + Coder.encryptMessage(message).orElse(null); // check also for security errors just to be sure - if (!encryptedData.isPresent() || + if (encryptedData == null || !message.getCoderStatus().getErrors().isEmpty()) { LOGGER.warning("encryption failed"); message.setStatus(KonMessage.Status.ERROR); mControl.handleSecurityErrors(message); return false; } - protoMessage.addExtension(new E2EEncryption(encryptedData.get())); + protoMessage.addExtension(new E2EEncryption(encryptedData)); } // transmission specific @@ -137,17 +136,14 @@ private static Message rawMessage(MessageContent content, Chat chat, boolean enc smackMessage.setBody(content.getPlainText()); // attachment - Optional optAtt = content.getAttachment(); - if (optAtt.isPresent()) { - MessageContent.Attachment att = optAtt.get(); - + MessageContent.Attachment att = content.getAttachment().orElse(null); + if (att != null) { OutOfBandData oobData = new OutOfBandData(att.getURL().toString(), att.getMimeType(), att.getLength(), encrypted); smackMessage.addExtension(oobData); - Optional optPreview = content.getPreview(); - if (optPreview.isPresent()) { - MessageContent.Preview preview = optPreview.get(); + MessageContent.Preview preview = content.getPreview().orElse(null); + if (preview != null) { String data = EncodingUtils.bytesToBase64(preview.getData()); BitsOfBinary bob = new BitsOfBinary(preview.getMimeType(), data); smackMessage.addExtension(bob); @@ -158,9 +154,9 @@ private static Message rawMessage(MessageContent content, Chat chat, boolean enc if (chat instanceof KonGroupChat) { KonGroupChat groupChat = (KonGroupChat) chat; KonGroupData gid = groupChat.getGroupData(); - Optional optGroupCommand = content.getGroupCommand(); - smackMessage.addExtension(optGroupCommand.isPresent() ? - ClientUtils.groupCommandToGroupExtension(groupChat, optGroupCommand.get()) : + MessageContent.GroupCommand groupCommand = content.getGroupCommand().orElse(null); + smackMessage.addExtension(groupCommand != null ? + ClientUtils.groupCommandToGroupExtension(groupChat, groupCommand) : new GroupExtension(gid.id, gid.owner.string())); } diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index 1b5cd69d..95809aca 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -89,12 +89,11 @@ public static enum Error { private static final HashMap KEY_MAP = new HashMap<>(); static PersonalKey myKeyOrNull() { - Optional optMyKey = Account.getInstance().getPersonalKey(); - if (!optMyKey.isPresent()) { + PersonalKey myKey = Account.getInstance().getPersonalKey().orElse(null); + if (myKey == null) LOGGER.log(Level.WARNING, "can't get personal key"); - return null; - } - return optMyKey.get(); + + return myKey; } public static Optional contactkey(Contact contact) { @@ -106,10 +105,10 @@ public static Optional contactkey(Contact contact) { byte[] rawKey = contact.getKey(); if (rawKey.length != 0) { - Optional optKey = PGPUtils.readPublicKey(rawKey); - if (optKey.isPresent()) { - KEY_MAP.put(contact, optKey.get()); - return optKey; + PGPCoderKey key = PGPUtils.readPublicKey(rawKey).orElse(null); + if (key != null) { + KEY_MAP.put(contact, key); + return Optional.of(key); } } diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 429d3af1..ee19ecf4 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -116,13 +116,13 @@ boolean decryptMessage() { // parse decrypted CPIM content String myUID = myKey.getUserId(); - Optional senderUID = senderKey.isPresent() ? - Optional.of(senderKey.get().userID) : - Optional.empty(); + String senderUID = senderKey.isPresent() ? + senderKey.get().userID : + null; String decrText = EncodingUtils.getString( plainOut.toByteArray(), CPIMMessage.CHARSET); - MessageContent content = this.parseCPIMOrNull(decrText, myUID, senderUID); + MessageContent content = this.parseCPIMOrNull(decrText, myUID, Optional.ofNullable(senderUID)); // set errors message.setSecurityErrors(allErrors); @@ -145,12 +145,11 @@ void decryptAttachment(Path baseDir) { } InMessage inMessage = (InMessage) message; - Optional optAttachment = inMessage.getContent().getAttachment(); - if (!optAttachment.isPresent()) { + MessageContent.Attachment attachment = inMessage.getContent().getAttachment().orElse(null); + if (attachment == null) { LOGGER.warning("no attachment in in-message"); return; } - MessageContent.Attachment attachment = optAttachment.get(); boolean loaded = this.loadKeys(); if (!loaded) diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index b3cc2a81..2f88265d 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -122,12 +122,11 @@ private Optional encryptData(String data, String mime) { } Optional encryptAttachment() { - Optional optAttachment = message.getContent().getAttachment(); - if (!optAttachment.isPresent()) { + MessageContent.Attachment attachment = message.getContent().getAttachment().orElse(null); + if (attachment == null) { LOGGER.warning("no attachment in out-message"); return Optional.empty(); } - MessageContent.Attachment attachment = optAttachment.get(); boolean loaded = this.loadKeys(); if (!loaded) @@ -173,10 +172,10 @@ private boolean loadKeys() { private static PGPUtils.PGPCoderKey[] receiverKeysOrNull(Contact[] contacts) { List keys = new ArrayList<>(contacts.length); for (Contact c : contacts) { - Optional optKey = Coder.contactkey(c); - if (!optKey.isPresent()) + PGPUtils.PGPCoderKey key = Coder.contactkey(c).orElse(null); + if (key == null) return null; - keys.add(optKey.get()); + keys.add(key); } return keys.toArray(new PGPUtils.PGPCoderKey[0]); } diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index f9c1b1b2..71a6722c 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -286,9 +286,9 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { int id = rs.getInt("_id"); String jsonGD = Database.getString(rs, Chat.COL_GD); - Optional optGD = Optional.ofNullable(jsonGD.isEmpty() ? + GroupMetaData gData = jsonGD.isEmpty() ? null : - GroupMetaData.fromJSONOrNull(jsonGD)); + GroupMetaData.fromJSONOrNull(jsonGD); String xmppID = Database.getString(rs, Chat.COL_XMPPID); @@ -296,9 +296,9 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { Map dbReceiver = Chat.loadReceiver(id); Set contacts = new HashSet<>(); for (int conID: dbReceiver.keySet()) { - Optional optCon = ContactList.getInstance().get(conID); - if (optCon.isPresent()) - contacts.add(optCon.get()); + Contact c = ContactList.getInstance().get(conID).orElse(null); + if (c != null) + contacts.add(c); else LOGGER.warning("can't find contact"); } @@ -310,8 +310,8 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String jsonViewSettings = Database.getString(rs, Chat.COL_VIEW_SET); - if (optGD.isPresent()) { - return GroupChat.create(id, contacts, optGD.get(), subject, read, jsonViewSettings); + if (gData != null) { + return GroupChat.create(id, contacts, gData, subject, read, jsonViewSettings); } else { if (contacts.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index f8df2f49..989f9926 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -119,9 +119,9 @@ public Optional get(GroupMetaData gData) { /** Find group chat by group data or create a new chat. */ public GroupChat getOrCreate(GroupMetaData gData, Contact contact) { - Optional optChat = this.get(gData, contact); - if (optChat.isPresent()) - return optChat.get(); + GroupChat chat = this.get(gData, contact).orElse(null); + if (chat != null) + return chat; return this.createNew(new Contact[]{contact}, gData, ""); } @@ -132,9 +132,9 @@ public Chat getOrCreate(Contact contact) { /** Find single chat for contact and XMPP ID or creates a new chat. */ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { - Optional optChat = this.get(contact, xmppThreadID); - if (optChat.isPresent()) - return optChat.get(); + SingleChat chat = this.get(contact, xmppThreadID).orElse(null); + if (chat != null) + return chat; return this.createNew(contact, xmppThreadID); } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index cd839a60..f154ab79 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -346,15 +346,13 @@ static Contact load(ResultSet rs) throws SQLException { String name = rs.getString(Contact.COL_NAME); String status = rs.getString(Contact.COL_STAT); long l = rs.getLong(Contact.COL_LAST_SEEN); - Optional lastSeen = l == 0 ? - Optional.empty() : - Optional.of(new Date(l)); + Date lastSeen = l == 0 ? null : new Date(l); boolean encr = rs.getBoolean(Contact.COL_ENCR); String key = Database.getString(rs, Contact.COL_PUB_KEY); String fp = Database.getString(rs, Contact.COL_KEY_FP); String avatarID = Database.getString(rs, Contact.COL_AVATAR_ID); - return new Contact(id, jid, name, status, lastSeen, encr, key, fp, avatarID); + return new Contact(id, jid, name, status, Optional.ofNullable(lastSeen), encr, key, fp, avatarID); } /** diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index e7636471..7424541f 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -97,11 +97,11 @@ public Optional create(JID jid, String name) { } Optional get(int id) { - Optional optContact = Optional.ofNullable(mIDMap.get(id)); - if (!optContact.isPresent()) { + Contact contact = mIDMap.get(id); + if (contact == null) { LOGGER.warning("can't find contact with ID: " + id); } - return optContact; + return Optional.ofNullable(contact); } /** @@ -118,9 +118,9 @@ public Optional get(JID jid) { */ public Optional getMe() { JID myJID = JID.me(); - Optional optContact = this.get(myJID); - if (optContact.isPresent()) - return optContact; + Contact contact = this.get(myJID).orElse(null); + if (contact == null) + return Optional.empty(); return this.create(myJID, ""); } diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 2ad41152..918db3a6 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -22,7 +22,6 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.logging.Logger; @@ -170,12 +169,12 @@ public void applyGroupCommand(MessageContent.GroupCommand command, Contact sende boolean meIn = false; for (JID jid: command.getAdded()) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't find contact, jid: "+jid); continue; } - Contact contact = optContact.get(); + if (mContactMap.keySet().contains(contact)) { LOGGER.warning("contact already in chat: "+contact); continue; @@ -198,20 +197,20 @@ public void applyGroupCommand(MessageContent.GroupCommand command, Contact sende break; case SET: for (JID jid : command.getAdded()) { - Optional optC = ContactList.getInstance().get(jid); - if (optC.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't get added contact, jid="+jid); continue; } - this.addContactSilent(optC.get()); + this.addContactSilent(contact); } for (JID jid : command.getRemoved()) { - Optional optC = ContactList.getInstance().get(jid); - if (optC.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't get removed contact, jid="+jid); continue; } - this.removeContactSilent(optC.get()); + this.removeContactSilent(contact); } mSubject = command.getSubject(); this.save(); diff --git a/src/main/java/org/kontalk/model/InMessage.java b/src/main/java/org/kontalk/model/InMessage.java index bd48e0f6..6dd093d1 100644 --- a/src/main/java/org/kontalk/model/InMessage.java +++ b/src/main/java/org/kontalk/model/InMessage.java @@ -122,14 +122,14 @@ public void setDecryptedAttachment(String filename) { } public void setPreviewFilename(String filename) { - Optional optPreview = this.getContent().getPreview(); - if (!optPreview.isPresent()) { + Preview preview = this.getContent().getPreview().orElse(null); + if (preview == null) { LOGGER.warning("no preview !?"); return; } - optPreview.get().setFilename(filename); + preview.setFilename(filename); this.save(); - this.changed(optPreview.get()); + this.changed(preview); } @Override diff --git a/src/main/java/org/kontalk/model/KonMessage.java b/src/main/java/org/kontalk/model/KonMessage.java index 831731d7..0212381a 100644 --- a/src/main/java/org/kontalk/model/KonMessage.java +++ b/src/main/java/org/kontalk/model/KonMessage.java @@ -213,12 +213,12 @@ public void setAttachmentErrors(EnumSet errors) { } protected MessageContent.Attachment getAttachment() { - Optional optAttachment = this.getContent().getAttachment(); - if (!optAttachment.isPresent()) { + MessageContent.Attachment att = this.getContent().getAttachment().orElse(null); + if (att == null) { LOGGER.warning("no attachment!?"); return null; } - return optAttachment.get(); + return att; } public CoderStatus getCoderStatus() { diff --git a/src/main/java/org/kontalk/model/Transmission.java b/src/main/java/org/kontalk/model/Transmission.java index a4882fa4..606f8777 100644 --- a/src/main/java/org/kontalk/model/Transmission.java +++ b/src/main/java/org/kontalk/model/Transmission.java @@ -150,8 +150,8 @@ private static Transmission load(ResultSet resultSet) throws SQLException { int id = resultSet.getInt("_id"); int contactID = resultSet.getInt(COL_CONTACT_ID); - Optional optContact = ContactList.getInstance().get(contactID); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(contactID).orElse(null); + if (contact == null) { LOGGER.warning("can't find contact in db, id: "+contactID); return null; } @@ -159,6 +159,6 @@ private static Transmission load(ResultSet resultSet) throws SQLException { long rDate = resultSet.getLong(COL_REC_DATE); Date receivedDate = rDate == 0 ? null : new Date(rDate); - return new Transmission(id, optContact.get(), jid, receivedDate); + return new Transmission(id, contact, jid, receivedDate); } } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index cfe9f591..106555d4 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -135,23 +135,19 @@ void queueDownload(InMessage message) { } private void uploadAsync(OutMessage message) { - Optional optAttachment = message.getContent().getAttachment(); - if (!optAttachment.isPresent()) { + Attachment attachment = message.getContent().getAttachment().orElse(null); + if (attachment == null) { LOGGER.warning("no attachment in message to upload"); return; } - Attachment attachment = optAttachment.get(); // if text will be encrypted, always encrypt attachment too boolean encrypt = message.getCoderStatus().getEncryption() == Encryption.DECRYPTED; - File file; - if (encrypt){ - Optional optFile = Coder.encryptAttachment(message); - if (!optFile.isPresent()) - return; - file = optFile.get(); - } else - file = attachment.getFile().toFile(); + File file = encrypt ? + Coder.encryptAttachment(message).orElse(null) : + attachment.getFile().toFile(); + if (file == null) + return; HTTPFileClient client = createClientOrNull(); if (client == null) @@ -189,12 +185,11 @@ private void uploadAsync(OutMessage message) { } private void downloadAsync(final InMessage message) { - Optional optAttachment = message.getContent().getAttachment(); - if (!optAttachment.isPresent()) { + Attachment attachment = message.getContent().getAttachment().orElse(null); + if (attachment == null) { LOGGER.warning("no attachment in message to download"); return; } - Attachment attachment = optAttachment.get(); HTTPFileClient client = createClientOrNull(); if (client == null) @@ -236,12 +231,11 @@ public void updateProgress(int p) { } public void savePreview(InMessage message) { - Optional optPreview = message.getContent().getPreview(); - if (!optPreview.isPresent()) { + Preview preview = message.getContent().getPreview().orElse(null); + if (preview == null) { LOGGER.warning("no preview in message: "+message); return; } - Preview preview = optPreview.get(); String id = Integer.toString(message.getID()); String dotExt = MediaUtils.extensionForMIME(preview.getMimeType()); String filename = id + "_bob" + dotExt; @@ -251,12 +245,11 @@ public void savePreview(InMessage message) { } boolean createImagePreview(KonMessage message) { - Optional optAtt = message.getContent().getAttachment(); - if (!optAtt.isPresent()) { + Attachment att = message.getContent().getAttachment().orElse(null); + if (att == null) { LOGGER.warning("no attachment in message: "+message); return false; } - Attachment att = optAtt.get(); Path path = filePath(att); if (!isImage(att.getMimeType())) @@ -298,11 +291,10 @@ Path filePath(Attachment attachment) { } Optional imagePreviewPath(KonMessage message) { - Optional optPreview = message.getContent().getPreview(); - if (!optPreview.isPresent()) + MessageContent.Preview preview = message.getContent().getPreview().orElse(null); + if (preview == null) return Optional.empty(); - MessageContent.Preview preview = optPreview.get(); String fn = preview.getFilename(); if (fn.isEmpty() || !isImage(preview.getMimeType())) return Optional.empty(); @@ -378,12 +370,11 @@ static Attachment attachmentOrNull(Path path) { } private static HTTPFileClient createClientOrNull(){ - Optional optKey = Account.getInstance().getPersonalKey(); - if (!optKey.isPresent()) { + PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); + if (key == null) { LOGGER.log(Level.WARNING, "personal key not loaded"); return null; } - PersonalKey key = optKey.get(); return new HTTPFileClient(key.getServerLoginKey(), key.getBridgeCertificate(), diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index bda3e207..b083785d 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -21,7 +21,6 @@ import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.logging.Logger; import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; @@ -52,20 +51,19 @@ public final class AvatarHandler { } public void onNotify(JID jid, String id) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; } - Contact contact = optContact.get(); if (id.isEmpty()) { // contact disabled avatar publishing // TODO } - Optional optAvatar = contact.getAvatar(); - if (optAvatar.isPresent() && optAvatar.get().id.equals(id)) + Avatar avatar = contact.getAvatar().orElse(null); + if (avatar != null && avatar.id.equals(id)) // avatar is not new return; @@ -78,8 +76,8 @@ public void onData(JID jid, String id, byte[] avatarData) { if (avatarData.length > MAX_SIZE) LOGGER.info("avatar data too long: "+avatarData.length); - final Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; } @@ -89,10 +87,10 @@ public void onData(JID jid, String id, byte[] avatarData) { return; } - Optional optImg = MediaUtils.readImage(avatarData); - if (!optImg.isPresent()) + BufferedImage img = MediaUtils.readImage(avatarData).orElse(null); + if (img == null) return; - optContact.get().setAvatar(new Avatar(id, optImg.get())); + contact.setAvatar(new Avatar(id, img)); } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 07f59839..3a0c1efb 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -163,12 +163,11 @@ public boolean newInMessage(MessageIDs ids, MessageContent content) { LOGGER.info("new incoming message, "+ids); - Optional optContact = this.getOrCreateContact(ids.jid); - if (!optContact.isPresent()) { + Contact contact = this.getOrCreateContact(ids.jid).orElse(null); + if (contact == null) { LOGGER.warning("can't get contact for message"); return false; } - Contact contact = optContact.get(); // decrypt message now to get group id ProtoMessage protoMessage = new ProtoMessage(contact, content); @@ -177,12 +176,12 @@ public boolean newInMessage(MessageIDs ids, } // NOTE: decryption must be successful to select group chat - Optional optGID = protoMessage.getContent().getGroupData(); + KonGroupData gData = protoMessage.getContent().getGroupData().orElse(null); // TODO ignore message if it contains unexpected group commands - Chat chat = optGID.isPresent() ? - ChatList.getInstance().getOrCreate(optGID.get(), contact) : + Chat chat = gData != null ? + ChatList.getInstance().getOrCreate(gData, contact) : ChatList.getInstance().getOrCreate(contact, ids.xmppThreadID); InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, @@ -203,11 +202,11 @@ public boolean newInMessage(MessageIDs ids, return false; } - Optional optCom = newMessage.getContent().getGroupCommand(); - if (optCom.isPresent()) { + GroupCommand com = newMessage.getContent().getGroupCommand().orElse(null); + if (com != null) { if (chat instanceof GroupChat) { mGroupControl.getInstanceFor((GroupChat) chat) - .onInMessage(optCom.get(), contact); + .onInMessage(com, contact); } else { LOGGER.warning("group command for non-group chat"); } @@ -221,26 +220,26 @@ public boolean newInMessage(MessageIDs ids, } public void messageSent(MessageIDs ids) { - Optional optMessage = findMessage(ids); - if (!optMessage.isPresent()) + OutMessage message = findMessage(ids).orElse(null); + if (message == null) return; - optMessage.get().setStatus(KonMessage.Status.SENT); + message.setStatus(KonMessage.Status.SENT); } public void setReceived(MessageIDs ids) { - Optional optMessage = findMessage(ids); - if (!optMessage.isPresent()) + OutMessage message = findMessage(ids).orElse(null); + if (message == null) return; - optMessage.get().setReceived(ids.jid); + message.setReceived(ids.jid); } public void setMessageError(MessageIDs ids, Condition condition, String errorText) { - Optional optMessage = findMessage(ids); - if (!optMessage.isPresent()) + OutMessage message = findMessage(ids).orElse(null); + if (message == null) return ; - optMessage.get().setServerError(condition.toString(), errorText); + message.setServerError(condition.toString(), errorText); } /** @@ -256,37 +255,35 @@ public void processChatState(MessageIDs ids, return; } } - Optional optContact = ContactList.getInstance().get(ids.jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(ids.jid).orElse(null); + if (contact == null) { LOGGER.info("can't find contact with jid: "+ids.jid); return; } - Contact contact = optContact.get(); // TODO chat states for group chats? - Optional optChat = ChatList.getInstance().get(contact, ids.xmppThreadID); - if (!optChat.isPresent()) + SingleChat chat = ChatList.getInstance().get(contact, ids.xmppThreadID).orElse(null); + if (chat == null) return; - optChat.get().setChatState(contact, chatState); + chat.setChatState(contact, chatState); } public void handlePGPKey(JID jid, byte[] rawKey) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.warning("can't find contact with jid: "+jid); return; } - this.handlePGPKey(optContact.get(), rawKey); + this.handlePGPKey(contact, rawKey); } void handlePGPKey(Contact contact, byte[] rawKey) { - Optional optKey = PGPUtils.readPublicKey(rawKey); - if (!optKey.isPresent()) { + PGPCoderKey key = PGPUtils.readPublicKey(rawKey).orElse(null); + if (key == null) { LOGGER.warning("invalid public PGP key, contact: "+contact); return; } - PGPCoderKey key = optKey.get(); if (!key.userID.contains("<"+contact.getJID().string()+">")) { LOGGER.warning("UID does not contain contact JID"); @@ -331,12 +328,11 @@ public void setBlockedContacts(JID[] jids) { } public void setContactBlocking(JID jid, boolean blocking) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.info("ignoring blocking of JID not in contact list"); return; } - Contact contact = optContact.get(); LOGGER.info("set contact blocking: "+contact+" "+blocking); contact.setBlocked(blocking); @@ -402,9 +398,9 @@ void maySendKeyRequest(Contact contact) { } Optional getOrCreateContact(JID jid) { - Optional optContact = ContactList.getInstance().get(jid); - if (optContact.isPresent()) - return optContact; + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact != null) + return Optional.of(contact); return this.createContact(jid, ""); } @@ -429,13 +425,12 @@ private Optional createContact(JID jid, String name, boolean encrypted) name = jid.local(); } - Optional optNewContact = ContactList.getInstance().create(jid, name); - if (!optNewContact.isPresent()) { + Contact newContact = ContactList.getInstance().create(jid, name).orElse(null); + if (newContact == null) { LOGGER.warning("can't create new contact"); // TODO tell view return Optional.empty(); } - Contact newContact = optNewContact.get(); newContact.setEncrypted(encrypted); @@ -509,22 +504,22 @@ private static Optional findMessage(MessageIDs ids) { ChatList cl = ChatList.getInstance(); // get chat by jid -> thread ID -> message id - Optional optContact = ContactList.getInstance().get(ids.jid); - if (optContact.isPresent()) { - Optional optChat = cl.get(optContact.get(), ids.xmppThreadID); - if (optChat.isPresent()) { - Optional optM = optChat.get().getMessages().getLast(ids.xmppID); - if (optM.isPresent()) - return optM; + Contact contact = ContactList.getInstance().get(ids.jid).orElse(null); + if (contact != null) { + Chat chat = cl.get(contact, ids.xmppThreadID).orElse(null); + if (chat != null) { + OutMessage m = chat.getMessages().getLast(ids.xmppID).orElse(null); + if (m != null) + return Optional.of(m); } } // fallback: search everywhere LOGGER.info("fallback search, IDs: "+ids); for (Chat chat: cl) { - Optional optM = chat.getMessages().getLast(ids.xmppID); - if (optM.isPresent()) - return optM; + OutMessage m = chat.getMessages().getLast(ids.xmppID).orElse(null); + if (m != null) + return Optional.of(m); } LOGGER.warning("can't find message by IDs: "+ids); @@ -669,12 +664,11 @@ public Chat getOrCreateSingleChat(Contact contact) { } public void createGroupChat(List contacts, String subject) { - Optional optMe = ContactList.getInstance().getMe(); - if (!optMe.isPresent()) { + Contact me = ContactList.getInstance().getMe().orElse(null); + if (me == null) { LOGGER.warning("can't find myself"); return; } - Contact me = optMe.get(); // user should be part of the group List withMe = new ArrayList<>(contacts); @@ -753,9 +747,9 @@ private void sendTextMessage(Chat chat, String text, Path file) { private PersonalKey keyOrNull(char[] password) { Account account = Account.getInstance(); - Optional optKey = account.getPersonalKey(); - if (optKey.isPresent()) - return optKey.get(); + PersonalKey key = account.getPersonalKey().orElse(null); + if (key != null) + return key; if (password.length == 0) { if (account.isPasswordProtected()) { diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 3f86c667..9cae548d 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -332,8 +332,7 @@ private static void setValue(PreparedStatement stat, int i, Object value) } else if (value instanceof EnumSet) { stat.setInt(i+1, EncodingUtils.enumSetToInt(((EnumSet) value))); } else if (value instanceof Optional) { - Optional o = (Optional) value; - setValue(stat, i, o.orElse(null)); + setValue(stat, i, ((Optional) value).orElse(null)); } else if (value instanceof JID) { stat.setString(i+1, ((JID) value).string()); } else if (value == null) { diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 2a1fce12..86349f68 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Optional; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; @@ -76,12 +75,12 @@ public void onEntryAdded(JID jid, name = ""; } - Optional optNewContact = mControl.createContact(jid, name); - if (!optNewContact.isPresent()) + Contact newContact = mControl.createContact(jid, name).orElse(null); + if (newContact == null) return; Contact.Subscription status = rosterToModelSubscription(itemStatus, type); - optNewContact.get().setSubScriptionStatus(status); + newContact.setSubScriptionStatus(status); if (status == Contact.Subscription.UNSUBSCRIBED) mControl.sendPresenceSubscription(jid, Client.PresenceCommand.REQUEST); @@ -89,25 +88,24 @@ public void onEntryAdded(JID jid, public void onEntryDeleted(JID jid) { // note: also called on rename - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; } - mControl.getViewControl().changed(new ViewEvent.ContactDeleted(optContact.get())); + mControl.getViewControl().changed(new ViewEvent.ContactDeleted(contact)); } public void onEntryUpdate(JID jid, String name, RosterPacket.ItemType type, RosterPacket.ItemStatus itemStatus) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { - LOGGER.warning("can't find contact with jid: "+jid); + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { + LOGGER.info("can't find contact with jid: "+jid); return; } - Contact contact = optContact.get(); // subcription may have changed contact.setSubScriptionStatus(rosterToModelSubscription(itemStatus, type)); @@ -117,10 +115,9 @@ public void onEntryUpdate(JID jid, } public void onSubscriptionRequest(JID jid, byte[] rawKey) { - Optional optContact = mControl.getOrCreateContact(jid); - if (!optContact.isPresent()) + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) return; - Contact contact = optContact.get(); if (Config.getInstance().getBoolean(Config.NET_AUTO_SUBSCRIPTION)) { mControl.sendPresenceSubscription(jid, Client.PresenceCommand.GRANT); @@ -138,23 +135,21 @@ public void onPresenceUpdate(JID jid, Presence.Type type, String status) { // don't wanna see myself return; - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { - LOGGER.warning("can't find contact with jid: "+jid); + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { + LOGGER.info("can't find contact with jid: "+jid); return; } - optContact.get().setOnline(type, status); + contact.setOnline(type, status); } public void onFingerprintPresence(JID jid, String fingerprint) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { - if (!this.isMe(jid)) - LOGGER.warning("can't find contact with jid:" + jid); + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { + LOGGER.info("can't find contact with jid: "+jid); return; } - Contact contact = optContact.get(); if (!fingerprint.isEmpty() && !fingerprint.equalsIgnoreCase(contact.getFingerprint())) { LOGGER.info("detected public key change, requesting new key..."); @@ -164,21 +159,19 @@ public void onFingerprintPresence(JID jid, String fingerprint) { // TODO key IDs can be forged, searching by it is defective by design public void onSignaturePresence(JID jid, String signature) { - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { - if (!this.isMe(jid)) - LOGGER.warning("can't find contact with jid:" + jid); + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { + LOGGER.info("can't find contact with jid: "+jid); return; } - Contact contact = optContact.get(); long keyID = PGPUtils.parseKeyIDFromSignature(signature); if (keyID == 0) return; if (contact.hasKey()) { - Optional optKey = Coder.contactkey(contact); - if (optKey.isPresent() && optKey.get().signKey.getKeyID() == keyID) + PGPUtils.PGPCoderKey key = Coder.contactkey(contact).orElse(null); + if (key != null && key.signKey.getKeyID() == keyID) // already have this key return; } @@ -194,18 +187,16 @@ public void onSignaturePresence(JID jid, String signature) { if (foundKey.isEmpty()) return; - Optional optKey = PGPUtils.readPublicKey(foundKey); - if (!optKey.isPresent()) + PGPUtils.PGPCoderKey key = PGPUtils.readPublicKey(foundKey).orElse(null); + if (key == null) return; - PGPUtils.PGPCoderKey key = optKey.get(); - if (key.signKey.getKeyID() != keyID) { LOGGER.warning("key ID is not what we were searching for"); return; } - mControl.getViewControl().changed(new ViewEvent.NewKey(optContact.get(), key)); + mControl.getViewControl().changed(new ViewEvent.NewKey(contact, key)); } public void onPresenceError(JID jid, XMPPError.Type type, XMPPError.Condition condition) { @@ -223,13 +214,11 @@ public void onPresenceError(JID jid, XMPPError.Type type, XMPPError.Condition co return; } - Optional optContact = ContactList.getInstance().get(jid); - if (!optContact.isPresent()) { - if (!this.isMe(jid)) - LOGGER.warning("can't find contact with jid:" + jid); + Contact contact = ContactList.getInstance().get(jid).orElse(null); + if (contact == null) { + LOGGER.info("can't find contact with jid: "+jid); return; } - Contact contact = optContact.get(); if (contact.getOnline() == Contact.Online.ERROR) // we already know this @@ -243,8 +232,8 @@ public void onPresenceError(JID jid, XMPPError.Type type, XMPPError.Condition co /* private */ private boolean isMe(JID jid) { - Optional optMyJID = mClient.getOwnJID(); - return optMyJID.isPresent() ? optMyJID.get().equals(jid) : false; + JID myJID = mClient.getOwnJID().orElse(null); + return myJID != null ? myJID.equals(jid) : false; } private static Contact.Subscription rosterToModelSubscription( diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 73c1f259..4edccf7b 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -83,9 +83,9 @@ private static void play(String fileName) { } public static BufferedImage readImage(Path path) { - Optional optImg = readImage(path.toFile()); - return optImg.isPresent() ? - optImg.get() : + BufferedImage img = readImage(path.toFile()).orElse(null); + return img != null ? + img : new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB); } diff --git a/src/main/java/org/kontalk/view/ChatDetails.java b/src/main/java/org/kontalk/view/ChatDetails.java index 56ec6e91..7915479f 100644 --- a/src/main/java/org/kontalk/view/ChatDetails.java +++ b/src/main/java/org/kontalk/view/ChatDetails.java @@ -36,7 +36,6 @@ import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.List; -import java.util.Optional; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.kontalk.model.Chat; @@ -95,8 +94,8 @@ final class ChatDetails extends WebPanel { groupPanel.add(new WebLabel(Tr.tr("Custom Background"))); mColorOpt = new WebRadioButton(Tr.tr("Color:") + " "); - Optional optBGColor = mChat.getViewSettings().getBGColor(); - mColorOpt.setSelected(optBGColor.isPresent()); + Color bgColor = mChat.getViewSettings().getBGColor().orElse(null); + mColorOpt.setSelected(bgColor != null); mColorOpt.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { @@ -105,7 +104,7 @@ public void itemStateChanged(ItemEvent e) { }); mColor = new WebButton(); mColor.setMinimumHeight(25); - Color oldColor = optBGColor.orElse(DEFAULT_BG); + Color oldColor = bgColor != null ? bgColor : DEFAULT_BG; mColor.setBottomBgColor(oldColor); groupPanel.add(new GroupPanel(GroupingType.fillLast, mColorOpt, @@ -115,7 +114,7 @@ public void itemStateChanged(ItemEvent e) { colorSlider.setMaximum(100); colorSlider.setPaintTicks(false); colorSlider.setPaintLabels(false); - colorSlider.setEnabled(optBGColor.isPresent()); + colorSlider.setEnabled(bgColor != null); final GradientData gradientData = GradientData.getDefaultValue(); // TODO set location for color gradientData.getColor(0); diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 11f2394a..b507bbcd 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -28,7 +28,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.Timer; import javax.swing.ListSelectionModel; @@ -82,19 +81,19 @@ public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) return; - Optional optChat = ChatListView.this.getSelectedValue(); - if (!optChat.isPresent()) { + Chat chat = ChatListView.this.getSelectedValue().orElse(null); + if (chat == null) { // note: this happens also on righ-click for some reason return; } // if event is caused by filtering, dont do anything - if (lastChat == optChat.get()) + if (lastChat == chat) return; mView.clearSearch(); - mView.showChat(optChat.get()); - lastChat = optChat.get(); + mView.showChat(chat); + lastChat = chat; } }); @@ -260,8 +259,8 @@ private void updateBG() { @Override protected boolean contains(String search) { // always show entry for current chat - Optional optChat = mView.getCurrentShownChat(); - if (optChat.isPresent() && optChat.get() == mValue) + Chat chat = mView.getCurrentShownChat().orElse(null); + if (chat != null && chat == mValue) return true; for (Contact contact: mValue.getAllContacts()) { @@ -274,20 +273,20 @@ protected boolean contains(String search) { @Override public int compareTo(TableItem o) { - Optional m = this.mValue.getMessages().getLast(); - Optional oM = o.mValue.getMessages().getLast(); - if (m.isPresent() && oM.isPresent()) - return -m.get().getDate().compareTo(oM.get().getDate()); + KonMessage m = this.mValue.getMessages().getLast().orElse(null); + KonMessage oM = o.mValue.getMessages().getLast().orElse(null); + if (m != null && oM != null) + return -m.getDate().compareTo(oM.getDate()); return -Integer.compare(this.mValue.getID(), o.mValue.getID()); } } private static String lastActivity(Chat chat, boolean pretty) { - Optional optM = chat.getMessages().getLast(); - String lastActivity = !optM.isPresent() ? Tr.tr("no messages yet") : - pretty ? Utils.PRETTY_TIME.format(optM.get().getDate()) : - Utils.MID_DATE_FORMAT.format(optM.get().getDate()); + KonMessage m = chat.getMessages().getLast().orElse(null); + String lastActivity = m == null ? Tr.tr("no messages yet") : + pretty ? Utils.PRETTY_TIME.format(m.getDate()) : + Utils.MID_DATE_FORMAT.format(m.getDate()); return lastActivity; } } diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 88ccd107..2a2dae82 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -157,11 +157,11 @@ public void adjustmentValueChanged(AdjustmentEvent e) { @Override public void paintComponent(Graphics g) { super.paintComponent(g); - Optional optBG = - ChatView.this.getCurrentBackground().updateNowOrLater(); + BufferedImage bg = + ChatView.this.getCurrentBackground().updateNowOrLater().orElse(null); // if there is something to draw, draw it now even if its old - if (optBG.isPresent()) - g.drawImage(optBG.get(), 0, 0, this.getWidth(), this.getHeight(), null); + if (bg != null) + g.drawImage(bg, 0, 0, this.getWidth(), this.getHeight(), null); } }); @@ -283,9 +283,9 @@ void filterCurrentChat(String searchText) { } void showChat(Chat chat) { - Optional optOldChat = this.getCurrentChat(); - if (optOldChat.isPresent()) - optOldChat.get().deleteObserver(this); + Chat oldChat = this.getCurrentChat().orElse(null); + if (oldChat != null) + oldChat.deleteObserver(this); chat.addObserver(this); @@ -317,13 +317,11 @@ private Background getCurrentBackground() { MessageList view = this.currentMessageListOrNull(); if (view == null) return mDefaultBG; - Optional optBG = view.getBG(); - if (!optBG.isPresent()) - return mDefaultBG; - return optBG.get(); + Background bg = view.getBG().orElse(null); + return bg == null ? mDefaultBG : bg; } - Optional createBG(Chat.ViewSettings s){ + Optional createBG(Chat.ViewSettings s) { JViewport p = this.mScrollPane.getViewport(); if (s.getBGColor().isPresent()) { Color c = s.getBGColor().get(); @@ -408,12 +406,11 @@ private void updateOnEDT(Object arg) { } private void onChatChange() { - Optional optChat = this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = this.getCurrentChat().orElse(null); + if (chat == null) return; - Chat chat = optChat.get(); - // update if chat changes... + // update if chat changes... // avatar mAvatarLabel.setIcon(AvatarLoader.load(chat)); @@ -448,34 +445,33 @@ private void onChatChange() { } private void showPopup(final WebToggleButton invoker) { - Optional optChat = ChatView.this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = ChatView.this.getCurrentChat().orElse(null); + if (chat == null) return; if (mPopup == null) mPopup = new ComponentUtils.ModalPopup(invoker); mPopup.removeAll(); - mPopup.add(new ChatDetails(mView, mPopup, optChat.get())); + mPopup.add(new ChatDetails(mView, mPopup, chat)); mPopup.showPopup(); } private void onKeyTypeEvent(boolean empty) { this.updateSendButton(); - Optional optChat = this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = this.getCurrentChat().orElse(null); + if (chat == null) return; // workaround: clearing the text area is not a key event if (!empty) - mView.getControl().handleOwnChatStateEvent(optChat.get(), ChatState.composing); + mView.getControl().handleOwnChatStateEvent(chat, ChatState.composing); } private void updateSendButton() { - Optional optChat = this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = this.getCurrentChat().orElse(null); + if (chat == null) return; - Chat chat = optChat.get(); // enable if chat is valid... mSendButton.setEnabled(chat.isValid() && @@ -486,8 +482,8 @@ private void updateSendButton() { } private void sendMsg() { - Optional optChat = this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = this.getCurrentChat().orElse(null); + if (chat == null) // now current chat return; @@ -495,7 +491,7 @@ private void sendMsg() { // if (!attachments.isEmpty()) // mView.getControl().sendAttachment(optChat.get(), attachments.get(0).toPath()); // else - mView.getControl().sendText(optChat.get(), mSendTextArea.getText()); + mView.getControl().sendText(chat, mSendTextArea.getText()); mSendTextArea.setText(""); } @@ -507,11 +503,11 @@ private void showFileDialog() { File file = fileChooser.getSelectedFile(); fileChooser.setCurrentDirectory(file.toPath().getParent().toString()); - Optional optChat = this.getCurrentChat(); - if (!optChat.isPresent()) + Chat chat = this.getCurrentChat().orElse(null); + if (chat == null) return; - mView.getControl().sendAttachment(optChat.get(), file.toPath()); + mView.getControl().sendAttachment(chat, file.toPath()); } /** A background image of chat view with efficient async reloading. */ diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 3ce627d0..ba4e8755 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -42,7 +42,6 @@ import java.awt.event.ItemListener; import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Box; @@ -279,9 +278,9 @@ public void actionPerformed(ActionEvent e) { } private void updateFingerprint() { - Optional optKey = Account.getInstance().getPersonalKey(); - mFingerprintArea.setText(optKey.isPresent() ? - Utils.fingerprint(optKey.get().getFingerprint()) : + PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); + mFingerprintArea.setText(key != null ? + Utils.fingerprint(key.getFingerprint()) : "- " + Tr.tr("no key loaded") + " -"); } @@ -371,12 +370,11 @@ void onInvalidInput() { @Override public void actionPerformed(ActionEvent e) { char[] oldPassword = passPanel.getOldPassword(); - Optional optNewPass = passPanel.getNewPassword(); - if (!optNewPass.isPresent()) { + char[] newPassword = passPanel.getNewPassword().orElse(null); + if (newPassword == null) { LOGGER.warning("can't get new password"); return; } - char[] newPassword = optNewPass.get(); try { Account.getInstance().setPassword(oldPassword, newPassword); } catch(KonException ex) { diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index bca69e31..7b682668 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -32,7 +32,6 @@ import java.awt.event.MouseEvent; import java.util.HashSet; import java.util.Observer; -import java.util.Optional; import java.util.Set; import javax.swing.Box; import javax.swing.ListSelectionModel; @@ -70,11 +69,11 @@ final class ContactListView extends Table implements Obser this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { - Optional optContact = ContactListView.this.getSelectedValue(); - if (!optContact.isPresent()) + Contact contact = ContactListView.this.getSelectedValue().orElse(null); + if (contact == null) return; - mView.showContactDetails(optContact.get()); + mView.showContactDetails(contact); } }); @@ -83,9 +82,9 @@ public void valueChanged(ListSelectionEvent e) { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { - Optional optContact = ContactListView.this.getSelectedValue(); - if (optContact.isPresent()) - mView.showChat(optContact.get()); + Contact contact = ContactListView.this.getSelectedValue().orElse(null); + if (contact != null) + mView.showChat(contact); } } @Override diff --git a/src/main/java/org/kontalk/view/ImportDialog.java b/src/main/java/org/kontalk/view/ImportDialog.java index eb935194..e506d350 100644 --- a/src/main/java/org/kontalk/view/ImportDialog.java +++ b/src/main/java/org/kontalk/view/ImportDialog.java @@ -41,7 +41,6 @@ import java.util.List; import java.util.Observable; import java.util.Observer; -import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; @@ -377,10 +376,10 @@ private void mayAbort() { @Override protected void onNext() { - Optional optNewPass = mPassPanel.getNewPassword(); - if (optNewPass.isPresent() && optNewPass.get().length > 0) { + char[] newPass = mPassPanel.getNewPassword().orElse(null); + if (newPass != null && newPass.length > 0) { try { - Account.getInstance().setPassword(new char[0], optNewPass.get()); + Account.getInstance().setPassword(new char[0], newPass); } catch (KonException ex) { LOGGER.log(Level.WARNING, "can't set password", ex); return; diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index baf96a7e..72f1e2b2 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -483,9 +483,9 @@ private void updateStatus() { } else { // IN message Date receivedDate = mValue.getDate(); String rec = Utils.MID_DATE_FORMAT.format(receivedDate); - Optional sentDate = mValue.getServerDate(); - if (sentDate.isPresent()) { - String sent = Utils.MID_DATE_FORMAT.format(sentDate.get()); + Date sentDate = mValue.getServerDate().orElse(null); + if (sentDate != null) { + String sent = Utils.MID_DATE_FORMAT.format(sentDate); if (!sent.equals(rec)) html += Tr.tr("Sent:")+ " " + sent + "
"; } @@ -544,10 +544,9 @@ else if (enc == Coder.Encryption.DECRYPTED && // attachment / image, note: loading many images is very slow private void updateAttachment() { - Optional optAttachment = mValue.getContent().getAttachment(); - if (!optAttachment.isPresent()) + Attachment att = mValue.getContent().getAttachment().orElse(null); + if (att == null) return; - Attachment att = optAttachment.get(); if (mAttPanel == null) { mAttPanel = new AttachmentPanel(); @@ -555,8 +554,8 @@ private void updateAttachment() { } // image thumbnail preview - Optional optImagePath = mView.getControl().getImagePath(mValue); - Path image = optImagePath.isPresent() ? optImagePath.get() : Paths.get(""); + Path imagePath = mView.getControl().getImagePath(mValue).orElse(null); + Path image = imagePath != null ? imagePath : Paths.get(""); mAttPanel.setImage(image); // link to the file @@ -594,9 +593,9 @@ public void actionPerformed(ActionEvent event) { }); popupMenu.add(decryptMenuItem); } - Optional optAtt = m.getContent().getAttachment(); - if (optAtt.isPresent() && - optAtt.get().getFile().toString().isEmpty()) { + Attachment att = m.getContent().getAttachment().orElse(null); + if (att != null && + att.getFile().toString().isEmpty()) { WebMenuItem attMenuItem = new WebMenuItem(Tr.tr("Load")); attMenuItem.setToolTipText(Tr.tr("Retry downloading attachment")); attMenuItem.addActionListener(new ActionListener() { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 19265b5d..4b2f20d3 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -338,15 +338,15 @@ void clearSearch() { void tabPaneChanged(MainFrame.Tab tab) { if (tab == MainFrame.Tab.CHATS) { - Optional optChat = mChatListView.getSelectedValue(); - if (optChat.isPresent()) { - mContent.showChat(optChat.get()); + Chat chat = mChatListView.getSelectedValue().orElse(null); + if (chat != null) { + mContent.showChat(chat); return; } } else { - Optional optContact = mContactListView.getSelectedValue(); - if (optContact.isPresent()) { - mContent.showContact(optContact.get()); + Contact contact = mContactListView.getSelectedValue().orElse(null); + if (contact != null) { + mContent.showContact(contact); return; } } From f3eea0f0a43ea448232c851b1f1967fc3c0e5dd6 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 14 Jan 2016 17:19:58 +0100 Subject: [PATCH 057/257] model: removed more optionals --- src/main/java/org/kontalk/model/Chat.java | 34 +++---- src/main/java/org/kontalk/model/Contact.java | 10 +- .../java/org/kontalk/model/KonMessage.java | 13 +-- .../org/kontalk/model/MessageContent.java | 94 +++++++++---------- .../java/org/kontalk/model/OutMessage.java | 2 +- .../java/org/kontalk/model/Transmission.java | 12 +-- 6 files changed, 81 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 71a6722c..572a7da4 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -351,7 +351,7 @@ public class KonChatState { // the chat (not necessary according to XEP-0085), this makes the // extra date field a bit useless // TODO save last active date to DB - private Optional mLastActive = Optional.empty(); + private Date mLastActive = null; protected KonChatState(Contact contact) { mContact = contact; @@ -368,7 +368,7 @@ public ChatState getState() { protected void setState(ChatState state) { mState = state; if (mState == ChatState.active || mState == ChatState.composing) - mLastActive = Optional.of(new Date()); + mLastActive = new Date(); } } @@ -377,48 +377,48 @@ public static class ViewSettings { private static final String JSON_IMAGE_PATH = "img"; // background color, if set - private final Optional mOptColor; + private final Color mColor; // custom image, if set private final String mImagePath; private ViewSettings(Chat t, String json) { Object obj = JSONValue.parse(json); - Optional optColor; + Color color; String imagePath; try { Map map = (Map) obj; - optColor = map.containsKey(JSON_BG_COLOR) ? - Optional.of(new Color(((Long) map.get(JSON_BG_COLOR)).intValue())) : - Optional.empty(); + color = map.containsKey(JSON_BG_COLOR) ? + new Color(((Long) map.get(JSON_BG_COLOR)).intValue()) : + null; imagePath = map.containsKey(JSON_IMAGE_PATH) ? (String) map.get(JSON_IMAGE_PATH) : ""; } catch (NullPointerException | ClassCastException ex) { LOGGER.log(Level.WARNING, "can't parse JSON view settings", ex); - optColor = Optional.empty(); + color = null; imagePath = ""; } - mOptColor = optColor; + mColor = color; mImagePath = imagePath; } public ViewSettings() { - mOptColor = Optional.empty(); + mColor = null; mImagePath = ""; } public ViewSettings(Color color) { - mOptColor = Optional.of(color); + mColor = null; mImagePath = ""; } public ViewSettings(String imagePath) { - mOptColor = Optional.empty(); + mColor = null; mImagePath = imagePath; } public Optional getBGColor() { - return mOptColor; + return Optional.ofNullable(mColor); } public String getImagePath() { @@ -429,8 +429,8 @@ public String getImagePath() { @SuppressWarnings("unchecked") String toJSONString() { JSONObject json = new JSONObject(); - if (mOptColor.isPresent()) - json.put(JSON_BG_COLOR, mOptColor.get().getRGB()); + if (mColor != null) + json.put(JSON_BG_COLOR, mColor.getRGB()); if (!mImagePath.isEmpty()) json.put(JSON_IMAGE_PATH, mImagePath); return json.toJSONString(); @@ -443,14 +443,14 @@ public boolean equals(Object obj) { if (!(obj instanceof ViewSettings)) return false; ViewSettings o = (ViewSettings) obj; - return mOptColor.equals(o.mOptColor) && + return mColor.equals(o.mColor) && mImagePath.equals(o.mImagePath); } @Override public int hashCode() { int hash = 7; - hash = 37 * hash + Objects.hashCode(this.mOptColor); + hash = 37 * hash + Objects.hashCode(this.mColor); hash = 37 * hash + Objects.hashCode(this.mImagePath); return hash; } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index f154ab79..9b04044b 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -87,7 +87,7 @@ public static enum Subscription { private JID mJID; private String mName; private String mStatus = ""; - private Optional mLastSeen = Optional.empty(); + private Date mLastSeen = null; private Online mAvailable = Online.UNKNOWN; private boolean mEncrypted = true; private String mKey = ""; @@ -130,7 +130,7 @@ public static enum Subscription { mJID = jid; mName = name; mStatus = status; - mLastSeen = lastSeen; + mLastSeen = lastSeen.orElse(null); mEncrypted = encrypted; mKey = publicKey; mFingerprint = fingerprint.toLowerCase(); @@ -178,7 +178,7 @@ public String getStatus() { } public Optional getLastSeen() { - return mLastSeen; + return Optional.ofNullable(mLastSeen); } public boolean getEncrypted() { @@ -200,7 +200,7 @@ public Online getOnline() { public void setOnline(Presence.Type type, String status) { if (type == Presence.Type.available) { mAvailable = Online.YES; - mLastSeen = Optional.of(new Date()); + mLastSeen = new Date(); } else if (type == Presence.Type.unavailable) { mAvailable = Online.NO; } @@ -299,7 +299,7 @@ void setDeleted() { mJID = JID.deleted(mID); mName = ""; mStatus = ""; - mLastSeen = Optional.empty(); + mLastSeen = null; mEncrypted = false; mKey = ""; mFingerprint = ""; diff --git a/src/main/java/org/kontalk/model/KonMessage.java b/src/main/java/org/kontalk/model/KonMessage.java index 0212381a..e9b70b31 100644 --- a/src/main/java/org/kontalk/model/KonMessage.java +++ b/src/main/java/org/kontalk/model/KonMessage.java @@ -112,7 +112,7 @@ public static enum Status { // last timestamp of server transmission packet // incoming: (delayed) sent; outgoing: sent or error - protected Optional mServerDate; + protected Date mServerDate; protected Status mStatus; protected CoderStatus mCoderStatus; protected ServerError mServerError; @@ -128,7 +128,7 @@ protected KonMessage(Chat chat, mDate = new Date(); mContent = content; - mServerDate = serverDate; + mServerDate = serverDate.orElse(null); mStatus = status; mCoderStatus = coderStatus; mServerError = new ServerError(); @@ -192,7 +192,7 @@ public Date getDate() { } public Optional getServerDate() { - return mServerDate; + return Optional.ofNullable(mServerDate); } public Status getStatus() { @@ -374,7 +374,7 @@ protected static class Builder { protected Transmission[] mTransmissions = null; private String mXMPPID = null; - private Optional mServerDate = null; + private Date mServerDate = null; private CoderStatus mCoderStatus = null; private ServerError mServerError = null; @@ -393,14 +393,11 @@ private Builder(int id, private void transmissions(Transmission[] transmission) { mTransmissions = transmission; } private void xmppID(String xmppID) { mXMPPID = xmppID; } - private void serverDate(Date date) { mServerDate = Optional.of(date); } + private void serverDate(Date date) { mServerDate = date; } private void coderStatus(CoderStatus coderStatus) { mCoderStatus = coderStatus; } private void serverError(ServerError error) { mServerError = error; } private KonMessage build() { - if (mServerDate == null) - mServerDate = Optional.empty(); - if (mTransmissions == null || mXMPPID == null || mCoderStatus == null || diff --git a/src/main/java/org/kontalk/model/MessageContent.java b/src/main/java/org/kontalk/model/MessageContent.java index ddd2c188..8a23f17d 100644 --- a/src/main/java/org/kontalk/model/MessageContent.java +++ b/src/main/java/org/kontalk/model/MessageContent.java @@ -49,15 +49,15 @@ public class MessageContent { // encrypted content, empty string if not present private String mEncryptedContent; // attachment (file url, path and metadata) - private final Optional mOptAttachment; + private final Attachment mAttachment; // small preview file of attachment - private Optional mOptPreview; + private Preview mPreview; // group id - private final Optional mOptGroupData; + private final KonGroupData mGroupData; // group command - private final Optional mOptGroupCommand; + private final GroupCommand mGroupCommand; // decrypted message content - private Optional mOptDecryptedContent; + private MessageContent mDecryptedContent; private static final String JSON_PLAIN_TEXT = "plain_text"; private static final String JSON_ENC_CONTENT = "encrypted_content"; @@ -86,11 +86,11 @@ public static MessageContent groupCommand(GroupCommand group) { private MessageContent(Builder builder) { mPlainText = builder.mPlainText; mEncryptedContent = builder.mEncrypted; - mOptAttachment = Optional.ofNullable(builder.mAttachment); - mOptPreview = Optional.ofNullable(builder.mPreview); - mOptGroupData = Optional.ofNullable(builder.mGroupData); - mOptGroupCommand = Optional.ofNullable(builder.mGroup); - mOptDecryptedContent = Optional.ofNullable(builder.mDecrypted); + mAttachment = builder.mAttachment; + mPreview = builder.mPreview; + mGroupData = builder.mGroupData; + mGroupCommand = builder.mGroup; + mDecryptedContent = builder.mDecrypted; } /** @@ -99,8 +99,8 @@ private MessageContent(Builder builder) { * plain text either return an empty string. */ public String getText() { - if (mOptDecryptedContent.isPresent()) - return mOptDecryptedContent.get().getPlainText(); + if (mDecryptedContent != null) + return mDecryptedContent.getPlainText(); else return mPlainText; } @@ -110,11 +110,11 @@ public String getPlainText() { } public Optional getAttachment() { - if (mOptDecryptedContent.isPresent() && - mOptDecryptedContent.get().getAttachment().isPresent()) { - return mOptDecryptedContent.get().getAttachment(); + if (mDecryptedContent != null && + mDecryptedContent.getAttachment().isPresent()) { + return mDecryptedContent.getAttachment(); } - return mOptAttachment; + return Optional.ofNullable(mAttachment); } public String getEncryptedContent() { @@ -122,42 +122,42 @@ public String getEncryptedContent() { } public void setDecryptedContent(MessageContent decryptedContent) { - assert !mOptDecryptedContent.isPresent(); - mOptDecryptedContent = Optional.of(decryptedContent); + assert mDecryptedContent == null; + mDecryptedContent = decryptedContent; // deleting encrypted data! mEncryptedContent = ""; } public Optional getPreview() { - if (mOptDecryptedContent.isPresent() && - mOptDecryptedContent.get().getPreview().isPresent()) { - return mOptDecryptedContent.get().getPreview(); + if (mDecryptedContent != null && + mDecryptedContent.getPreview().isPresent()) { + return mDecryptedContent.getPreview(); } - return mOptPreview; + return Optional.ofNullable(mPreview); } void setPreview(Preview preview) { - if (mOptPreview.isPresent()) { + if (mPreview != null) { LOGGER.warning("preview already present, not overwriting"); return; } - mOptPreview = Optional.of(preview); + mPreview = preview; } public Optional getGroupData() { - if (mOptDecryptedContent.isPresent() && - mOptDecryptedContent.get().getGroupData().isPresent()) { - return mOptDecryptedContent.get().getGroupData(); + if (mDecryptedContent != null && + mDecryptedContent.getGroupData().isPresent()) { + return mDecryptedContent.getGroupData(); } - return mOptGroupData; + return Optional.ofNullable(mGroupData); } public Optional getGroupCommand() { - if (mOptDecryptedContent.isPresent() && - mOptDecryptedContent.get().getGroupCommand().isPresent()) { - return mOptDecryptedContent.get().getGroupCommand(); + if (mDecryptedContent != null && + mDecryptedContent.getGroupCommand().isPresent()) { + return mDecryptedContent.getGroupCommand(); } - return mOptGroupCommand; + return Optional.ofNullable(mGroupCommand); } /** @@ -167,21 +167,21 @@ public Optional getGroupCommand() { public boolean isEmpty() { return mPlainText.isEmpty() && mEncryptedContent.isEmpty() && - !mOptAttachment.isPresent() && - !mOptPreview.isPresent() && - !mOptDecryptedContent.isPresent() && - !mOptGroupCommand.isPresent(); + mAttachment == null && + mPreview == null && + mDecryptedContent == null && + mGroupCommand == null; } public boolean isComplex() { - return mOptAttachment.isPresent() || mOptGroupCommand.isPresent(); + return mAttachment != null || mGroupCommand != null; } @Override public String toString() { return "CONT:plain="+mPlainText+",encr="+mEncryptedContent - +",att="+mOptAttachment+",gd="+mOptGroupData+",gc="+mOptGroupCommand - +",decr="+mOptDecryptedContent; + +",att="+mAttachment+",gd="+mGroupData+",gc="+mGroupCommand + +",decr="+mDecryptedContent; } // using legacy lib, raw types extend Object @@ -191,19 +191,19 @@ String toJSON() { EncodingUtils.putJSON(json, JSON_PLAIN_TEXT, mPlainText); - if (mOptAttachment.isPresent()) - json.put(JSON_ATTACHMENT, mOptAttachment.get().toJSONString()); + if (mAttachment != null) + json.put(JSON_ATTACHMENT, mAttachment.toJSONString()); EncodingUtils.putJSON(json, JSON_ENC_CONTENT, mEncryptedContent); - if (mOptPreview.isPresent()) - json.put(JSON_PREVIEW, mOptPreview.get().toJSON()); + if (mPreview != null) + json.put(JSON_PREVIEW, mPreview.toJSON()); - if (mOptGroupCommand.isPresent()) - json.put(JSON_GROUP_COMMAND, mOptGroupCommand.get().toJSON()); + if (mGroupCommand != null) + json.put(JSON_GROUP_COMMAND, mGroupCommand.toJSON()); - if (mOptDecryptedContent.isPresent()) - json.put(JSON_DEC_CONTENT, mOptDecryptedContent.get().toJSON()); + if (mDecryptedContent != null) + json.put(JSON_DEC_CONTENT, mDecryptedContent.toJSON()); return json.toJSONString(); } diff --git a/src/main/java/org/kontalk/model/OutMessage.java b/src/main/java/org/kontalk/model/OutMessage.java index 91f5ba77..563d6d74 100644 --- a/src/main/java/org/kontalk/model/OutMessage.java +++ b/src/main/java/org/kontalk/model/OutMessage.java @@ -94,7 +94,7 @@ public void setStatus(Status status) { mStatus = status; if (status != Status.PENDING) - mServerDate = Optional.of(new Date()); + mServerDate = new Date(); this.save(); this.changed(mStatus); } diff --git a/src/main/java/org/kontalk/model/Transmission.java b/src/main/java/org/kontalk/model/Transmission.java index 606f8777..1c971e03 100644 --- a/src/main/java/org/kontalk/model/Transmission.java +++ b/src/main/java/org/kontalk/model/Transmission.java @@ -62,12 +62,12 @@ final public class Transmission { private final Contact mContact; private final JID mJID; - protected Optional mReceivedDate; + private Date mReceivedDate; Transmission(Contact contact, JID jid, int messageID) { mContact = contact; mJID = jid; - mReceivedDate = Optional.empty(); + mReceivedDate = null; mID = this.insert(messageID); } @@ -76,7 +76,7 @@ private Transmission(int id, Contact contact, JID jid, Date receivedDate) { mID = id; mContact = contact; mJID = jid; - mReceivedDate = Optional.ofNullable(receivedDate); + mReceivedDate = receivedDate; } public Contact getContact() { @@ -88,15 +88,15 @@ public JID getJID() { } public Optional getReceivedDate() { - return mReceivedDate; + return Optional.ofNullable(mReceivedDate); } public boolean isReceived() { - return mReceivedDate.isPresent(); + return mReceivedDate != null; } void setReceived(Date date) { - mReceivedDate = Optional.of(date); + mReceivedDate = date; this.save(); } From f68bcc5365fd33b9750d27214dc42b3d90a30599 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 15:18:12 +0100 Subject: [PATCH 058/257] view: added avatars to contact list view, closes #28 already --- .../java/org/kontalk/view/AvatarLoader.java | 11 +++++----- src/main/java/org/kontalk/view/ChatView.java | 9 +++++---- .../org/kontalk/view/ContactListView.java | 20 +++++++++++++++---- src/main/java/org/kontalk/view/View.java | 3 +++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index d5a1405a..f8efc1b0 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -28,7 +28,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import javax.swing.ImageIcon; import org.apache.commons.lang.ObjectUtils; import org.kontalk.model.Chat; import org.kontalk.model.Contact; @@ -47,21 +46,21 @@ final class AvatarLoader { private static final String FALLBACK_LETTER = "?"; private static final Color FALLBACK_COLOR = new Color(220, 220, 220); - private static final Map CACHE = new HashMap<>(); + private static final Map CACHE = new HashMap<>(); AvatarLoader() {}; - static ImageIcon load(Chat chat) { + static Image load(Chat chat) { return load(new Item(chat)); } - static ImageIcon load(Contact contact) { + static Image load(Contact contact) { return load(new Item(contact)); } - private static ImageIcon load(Item item) { + private static Image load(Item item) { if (!CACHE.containsKey(item)) { - CACHE.put(item, new ImageIcon(item.createImage())); + CACHE.put(item, item.createImage()); } return CACHE.get(item); } diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 2a2dae82..8bd8ed11 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -18,6 +18,7 @@ package org.kontalk.view; +import com.alee.extended.image.WebImage; import com.alee.extended.panel.GroupPanel; import com.alee.extended.panel.GroupingType; import com.alee.laf.button.WebButton; @@ -89,7 +90,7 @@ final class ChatView extends WebPanel implements Observer { private final View mView; - private final WebLabel mAvatarLabel; + private final WebImage mAvatar; private final WebLabel mTitleLabel; private final WebLabel mSubTitleLabel; private final WebScrollPane mScrollPane; @@ -112,8 +113,8 @@ final class ChatView extends WebPanel implements Observer { new BorderLayout(View.GAP_SMALL, View.GAP_SMALL)); titlePanel.setMargin(View.MARGIN_DEFAULT); - mAvatarLabel = new WebLabel(); - titlePanel.add(mAvatarLabel, BorderLayout.WEST); + mAvatar = new WebImage(); + titlePanel.add(mAvatar, BorderLayout.WEST); mTitleLabel = new WebLabel(); mTitleLabel.setFontSize(16); @@ -412,7 +413,7 @@ private void onChatChange() { // update if chat changes... // avatar - mAvatarLabel.setIcon(AvatarLoader.load(chat)); + mAvatar.setImage(AvatarLoader.load(chat)); // chat titles mTitleLabel.setText(Utils.chatTitle(chat)); diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 7b682668..fdc12038 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -18,6 +18,8 @@ package org.kontalk.view; +import com.alee.extended.image.DisplayType; +import com.alee.extended.image.WebImage; import com.alee.extended.panel.GroupPanel; import com.alee.extended.panel.GroupingType; import com.alee.laf.label.WebLabel; @@ -125,6 +127,7 @@ private void showPopupMenu(MouseEvent e) { /** One item in the contact list representing a contact. */ final class ContactItem extends Table.TableItem { + private final WebImage mAvatar; private final WebLabel mNameLabel; private final WebLabel mStatusLabel; private Color mBackround; @@ -136,16 +139,22 @@ final class ContactItem extends Table.TableItem { this.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_SMALL)); this.setMargin(View.MARGIN_SMALL); + mAvatar = new WebImage().setDisplayType(DisplayType.fitComponent); + mAvatar.setPreferredSize(View.AVATAR_LIST_DIM); + this.add(mAvatar, BorderLayout.WEST); + mNameLabel = new WebLabel("foo"); mNameLabel.setFontSize(14); - this.add(mNameLabel, BorderLayout.CENTER); mStatusLabel = new WebLabel("foo"); mStatusLabel.setForeground(Color.GRAY); mStatusLabel.setFontSize(11); - this.add(new GroupPanel(GroupingType.fillFirst, - Box.createGlue(), mStatusLabel), - BorderLayout.SOUTH); + this.add( + new GroupPanel(View.GAP_SMALL, false, + mNameLabel, + new GroupPanel(GroupingType.fillFirst, + Box.createGlue(), mStatusLabel) + ), BorderLayout.CENTER); this.updateOnEDT(null); } @@ -187,6 +196,9 @@ protected boolean contains(String search) { @Override protected void updateOnEDT(Object arg) { + // avatar + mAvatar.setImage(AvatarLoader.load(mValue)); + // name String name = Utils.displayName(mValue); if (!name.equals(mNameLabel.getText())) { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 4b2f20d3..023cbbee 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -41,6 +41,7 @@ import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import java.awt.BorderLayout; +import java.awt.Dimension; import org.kontalk.system.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Chat; @@ -77,6 +78,8 @@ public final class View implements Observer { static final Color LIGHT_GREEN = new Color(220, 250, 220); static final Color DARK_GREEN = new Color(0, 100, 0); + static final Dimension AVATAR_LIST_DIM = new Dimension(30, 30); + static final String REMOVE_CONTACT_NOTE = Tr.tr("Chats and messages will not be deleted."); private final ViewControl mControl; From 2720494e69c34053f5696cd415198db2e264b204 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 15:20:31 +0100 Subject: [PATCH 059/257] view: make fallback avatars less bright --- src/main/java/org/kontalk/view/AvatarLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index f8efc1b0..c5e0310a 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -148,7 +148,7 @@ private static BufferedImage fallback(String text, int colorCode, int size) { Color color; if (!text.isEmpty()) { int hue = Math.abs(colorCode) % 360; - color = Color.getHSBColor(hue / 360.0f, 1, 1); + color = Color.getHSBColor(hue / 360.0f, 0.8f, 1); } else { color = FALLBACK_COLOR; } From 4dc3cd8541f6d0dff34800142d157e24a1f58f87 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 16:35:13 +0100 Subject: [PATCH 060/257] view: added avatars also to chat list view --- .../java/org/kontalk/view/ChatListView.java | 46 +++++++++++++------ .../org/kontalk/view/ContactListView.java | 4 +- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index b507bbcd..a22c82d6 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -18,6 +18,10 @@ package org.kontalk.view; +import com.alee.extended.image.DisplayType; +import com.alee.extended.image.WebImage; +import com.alee.extended.panel.GroupPanel; +import com.alee.extended.panel.GroupingType; import com.alee.laf.label.WebLabel; import com.alee.laf.menu.WebMenuItem; import com.alee.laf.menu.WebPopupMenu; @@ -30,6 +34,7 @@ import java.util.HashSet; import java.util.Set; import java.util.Timer; +import javax.swing.Box; import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -159,6 +164,7 @@ private void deleteSelectedChat() { protected final class ChatItem extends Table.TableItem { + private final WebImage mAvatar; private final WebLabel mTitleLabel; private final WebLabel mStatusLabel; private final WebLabel mChatStateLabel; @@ -170,11 +176,14 @@ protected final class ChatItem extends Table.TableItem { this.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_SMALL)); this.setMargin(View.MARGIN_SMALL); + mAvatar = new WebImage().setDisplayType(DisplayType.fitComponent); + mAvatar.setPreferredSize(View.AVATAR_LIST_DIM); + this.add(mAvatar, BorderLayout.WEST); + mTitleLabel = new WebLabel(); mTitleLabel.setFontSize(14); if (mValue.isGroupChat()) mTitleLabel.setForeground(View.DARK_GREEN); - this.add(mTitleLabel, BorderLayout.NORTH); mStatusLabel = new WebLabel(); mStatusLabel.setForeground(Color.GRAY); @@ -186,7 +195,13 @@ protected final class ChatItem extends Table.TableItem { mChatStateLabel.setFontSize(13); mChatStateLabel.setBoldFont(); //mChatStateLabel.setMargin(0, 5, 0, 5); - this.add(mChatStateLabel, BorderLayout.WEST); + + this.add( + new GroupPanel(View.GAP_SMALL, false, + mTitleLabel, + new GroupPanel(GroupingType.fillFirst, + Box.createGlue(), mStatusLabel, mChatStateLabel) + ), BorderLayout.CENTER); this.updateView(null); @@ -221,6 +236,11 @@ private void updateView(Object arg) { mTitleLabel.setText(Utils.chatTitle(mValue)); } + // avatar may change when subject or contact name changes + if (arg == null || arg instanceof Contact || arg instanceof String) { + mAvatar.setImage(AvatarLoader.load(mValue)); + } + if (arg == null || arg instanceof KonMessage) { this.updateBG(); mStatusLabel.setText(lastActivity(mValue, true)); @@ -231,25 +251,25 @@ private void updateView(Object arg) { mStatusLabel.setText(lastActivity(mValue, true)); } + String stateText = ""; if (arg instanceof Chat.KonChatState) { KonChatState state = (KonChatState) arg; - String stateText = null; switch(state.getState()) { case composing: stateText = Tr.tr("is writing…"); break; //case paused: activity = T/r.tr("stopped typing"); break; //case inactive: stateText = T/r.tr("is inactive"); break; } - if (stateText == null) { - // 'inactive' is default - mChatStateLabel.setText(""); - return; - } - - if (mValue.isGroupChat()) - stateText = state.getContact().getName() + " " + stateText; - - mChatStateLabel.setText(stateText + " "); + if (!stateText.isEmpty() && mValue.isGroupChat()) + stateText = state.getContact().getName() + ": " + stateText; } + if (stateText.isEmpty()) { + mChatStateLabel.setText(""); + mStatusLabel.setVisible(true); + } else { + mChatStateLabel.setText(stateText); + mStatusLabel.setVisible(false); + } + } private void updateBG() { diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index fdc12038..d935f956 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -143,10 +143,10 @@ final class ContactItem extends Table.TableItem { mAvatar.setPreferredSize(View.AVATAR_LIST_DIM); this.add(mAvatar, BorderLayout.WEST); - mNameLabel = new WebLabel("foo"); + mNameLabel = new WebLabel(); mNameLabel.setFontSize(14); - mStatusLabel = new WebLabel("foo"); + mStatusLabel = new WebLabel(); mStatusLabel.setForeground(Color.GRAY); mStatusLabel.setFontSize(11); this.add( From 4f31e209782051dcfee76ced3971b7ff2a2b1daf Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 17:00:34 +0100 Subject: [PATCH 061/257] view: unified font size values --- src/main/java/org/kontalk/view/ChatListView.java | 6 +++--- src/main/java/org/kontalk/view/ChatView.java | 6 +++--- src/main/java/org/kontalk/view/ConfigurationDialog.java | 2 +- src/main/java/org/kontalk/view/ContactListView.java | 4 ++-- src/main/java/org/kontalk/view/MainFrame.java | 2 +- src/main/java/org/kontalk/view/MessageList.java | 6 +++--- src/main/java/org/kontalk/view/Notifier.java | 2 +- src/main/java/org/kontalk/view/View.java | 8 ++++++++ 8 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index a22c82d6..1b5ad64d 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -181,18 +181,18 @@ protected final class ChatItem extends Table.TableItem { this.add(mAvatar, BorderLayout.WEST); mTitleLabel = new WebLabel(); - mTitleLabel.setFontSize(14); + mTitleLabel.setFontSize(View.FONT_SIZE_BIG); if (mValue.isGroupChat()) mTitleLabel.setForeground(View.DARK_GREEN); mStatusLabel = new WebLabel(); mStatusLabel.setForeground(Color.GRAY); - mStatusLabel.setFontSize(11); + mStatusLabel.setFontSize(View.FONT_SIZE_TINY); this.add(mStatusLabel, BorderLayout.EAST); mChatStateLabel = new WebLabel(); mChatStateLabel.setForeground(View.GREEN); - mChatStateLabel.setFontSize(13); + mChatStateLabel.setFontSize(View.FONT_SIZE_NORMAL); mChatStateLabel.setBoldFont(); //mChatStateLabel.setMargin(0, 5, 0, 5); diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 8bd8ed11..a2b405fa 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -117,10 +117,10 @@ final class ChatView extends WebPanel implements Observer { titlePanel.add(mAvatar, BorderLayout.WEST); mTitleLabel = new WebLabel(); - mTitleLabel.setFontSize(16); + mTitleLabel.setFontSize(View.FONT_SIZE_HUGE); mTitleLabel.setDrawShade(true); mSubTitleLabel = new WebLabel(); - mSubTitleLabel.setFontSize(11); + mSubTitleLabel.setFontSize(View.FONT_SIZE_TINY); mSubTitleLabel.setForeground(Color.GRAY); titlePanel.add(new GroupPanel(View.GAP_SMALL, false, mTitleLabel, mSubTitleLabel), BorderLayout.CENTER); @@ -171,7 +171,7 @@ public void paintComponent(Graphics g) { mSendTextArea.setMargin(View.MARGIN_SMALL); mSendTextArea.setLineWrap(true); mSendTextArea.setWrapStyleWord(true); - mSendTextArea.setFontSize(13); + mSendTextArea.setFontSize(View.FONT_SIZE_NORMAL); mSendTextArea.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index ba4e8755..1895db94 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -76,7 +76,7 @@ private static enum ConfPage {MAIN, ACCOUNT}; this.setLayout(new BorderLayout(View.GAP_SMALL, View.GAP_SMALL)); WebTabbedPane tabbedPane = new WebTabbedPane(WebTabbedPane.LEFT); - tabbedPane.setFontSize(13); + tabbedPane.setFontSize(View.FONT_SIZE_NORMAL); final MainPanel mainPanel = new MainPanel(); final AccountPanel accountPanel = new AccountPanel(); final PrivacyPanel privacyPanel = new PrivacyPanel(); diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index d935f956..623901b9 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -144,11 +144,11 @@ final class ContactItem extends Table.TableItem { this.add(mAvatar, BorderLayout.WEST); mNameLabel = new WebLabel(); - mNameLabel.setFontSize(14); + mNameLabel.setFontSize(View.FONT_SIZE_BIG); mStatusLabel = new WebLabel(); mStatusLabel.setForeground(Color.GRAY); - mStatusLabel.setFontSize(11); + mStatusLabel.setFontSize(View.FONT_SIZE_TINY); this.add( new GroupPanel(View.GAP_SMALL, false, mNameLabel, diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index dcf9c9c5..1315e14b 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -233,7 +233,7 @@ public void actionPerformed(ActionEvent event) { mTabbedPane.setTabComponentAt(Tab.CONTACT.ordinal(), new WebVerticalLabel(Tr.tr("Contacts"))); // setSize() does not work, whatever - mTabbedPane.setPreferredSize(new Dimension(240, -1)); + mTabbedPane.setPreferredSize(new Dimension(View.LISTS_WIDTH, -1)); mTabbedPane.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 72f1e2b2..ded54272 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -251,7 +251,7 @@ private void createContent() { // from label if (mValue.isInMessage() && mValue.getChat().isGroupChat()) { mFromLabel = new WebLabel(); - mFromLabel.setFontSize(12); + mFromLabel.setFontSize(View.FONT_SIZE_SMALL); mFromLabel.setForeground(Color.BLUE); mFromLabel.setItalicFont(); mPanel.add(mFromLabel, BorderLayout.NORTH); @@ -264,7 +264,7 @@ private void createContent() { mTextPane = new WebTextPane(); mTextPane.setEditable(false); mTextPane.setOpaque(false); - //mTextPane.setFontSize(12); + //mTextPane.setFontSize(View.FONT_SIZE_SMALL); // sets default font mTextPane.putClientProperty(WebEditorPane.HONOR_DISPLAY_PROPERTIES, true); //for detecting clicks @@ -300,7 +300,7 @@ private void createContent() { // date label WebLabel dateLabel = new WebLabel(Utils.SHORT_DATE_FORMAT.format(mValue.getDate())); dateLabel.setForeground(Color.GRAY); - dateLabel.setFontSize(11); + dateLabel.setFontSize(View.FONT_SIZE_TINY); mStatusPanel.add(dateLabel); WebPanel southPanel = new WebPanel(); diff --git a/src/main/java/org/kontalk/view/Notifier.java b/src/main/java/org/kontalk/view/Notifier.java index 9e5b38af..265b5e05 100644 --- a/src/main/java/org/kontalk/view/Notifier.java +++ b/src/main/java/org/kontalk/view/Notifier.java @@ -245,7 +245,7 @@ public void closed() { panel.setMargin(View.MARGIN_DEFAULT); panel.setOpaque(false); WebLabel title = new WebLabel("A new Message!"); - title.setFontSize(14); + title.setFontSize(View.FONT_SIZE_BIG); title.setForeground(Color.WHITE); panel.add(title, BorderLayout.NORTH); String text = "this is some message, and some longer text was added"; diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 023cbbee..a6b10929 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -60,6 +60,8 @@ public final class View implements Observer { private static final Logger LOGGER = Logger.getLogger(View.class.getName()); + static final int LISTS_WIDTH = 270; + static final int GAP_DEFAULT = 10; static final int GAP_BIG = 15; static final int GAP_SMALL = 5; @@ -67,6 +69,12 @@ public final class View implements Observer { static final int MARGIN_BIG = 15; static final int MARGIN_SMALL = 5; + static final int FONT_SIZE_TINY = 11; + static final int FONT_SIZE_SMALL = 12; + static final int FONT_SIZE_NORMAL = 13; + static final int FONT_SIZE_BIG = 14; + static final int FONT_SIZE_HUGE = 16; + static final int MAX_SUBJ_LENGTH = 30; static final int MAX_NAME_LENGTH = 60; static final int MAX_JID_LENGTH = 100; From 6db649d767f4631530d8b5cf7881e502b8cac556 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 17:12:54 +0100 Subject: [PATCH 062/257] view: layout changes for chat list and chat view --- src/main/java/org/kontalk/view/ChatListView.java | 5 +++-- src/main/java/org/kontalk/view/ChatView.java | 2 +- src/main/java/org/kontalk/view/ContactListView.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 1b5ad64d..c0a0baac 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -173,8 +173,8 @@ protected final class ChatItem extends Table.TableItem { ChatItem(Chat chat) { super(chat); - this.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_SMALL)); - this.setMargin(View.MARGIN_SMALL); + this.setLayout(new BorderLayout(View.GAP_DEFAULT, 0)); + this.setMargin(View.MARGIN_DEFAULT); mAvatar = new WebImage().setDisplayType(DisplayType.fitComponent); mAvatar.setPreferredSize(View.AVATAR_LIST_DIM); @@ -182,6 +182,7 @@ protected final class ChatItem extends Table.TableItem { mTitleLabel = new WebLabel(); mTitleLabel.setFontSize(View.FONT_SIZE_BIG); + mTitleLabel.setDrawShade(true); if (mValue.isGroupChat()) mTitleLabel.setForeground(View.DARK_GREEN); diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index a2b405fa..e36477d0 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -110,7 +110,7 @@ final class ChatView extends WebPanel implements Observer { mView = view; WebPanel titlePanel = new WebPanel(false, - new BorderLayout(View.GAP_SMALL, View.GAP_SMALL)); + new BorderLayout(View.GAP_DEFAULT, 0)); titlePanel.setMargin(View.MARGIN_DEFAULT); mAvatar = new WebImage(); diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 623901b9..7dd09833 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -136,7 +136,7 @@ final class ContactItem extends Table.TableItem { super(contact); //this.setPaintFocus(true); - this.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_SMALL)); + this.setLayout(new BorderLayout(View.GAP_DEFAULT, 0)); this.setMargin(View.MARGIN_SMALL); mAvatar = new WebImage().setDisplayType(DisplayType.fitComponent); From 7f74103af799fa8d8279ef6a3ae46e146283afc9 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 17:41:14 +0100 Subject: [PATCH 063/257] moved code for setting user status text to control --- src/main/java/org/kontalk/client/Client.java | 12 ++++-------- src/main/java/org/kontalk/system/Control.java | 19 +++++++++++++++++-- .../java/org/kontalk/view/ComponentUtils.java | 16 +--------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index d75e3fc1..331d21e6 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -247,7 +247,6 @@ private void connectAsync() { // Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex); // } - this.sendInitialPresence(); this.sendBlocklistRequest(); mControl.setStatus(Control.Status.CONNECTED); @@ -308,14 +307,11 @@ public void sendBlockingCommand(JID jid, boolean blocking) { new BlockSendReceiver(mControl, mConn, blocking, jid).sendAndListen(); } - public void sendInitialPresence() { + public void sendUserPresence(String statusText) { Presence presence = new Presence(Presence.Type.available); - List stats = Config.getInstance().getList(Config.NET_STATUS_LIST); - if (!stats.isEmpty()) { - String stat = (String) stats.get(0); - if (!stat.isEmpty()) - presence.setStatus(stat); - } + if (!statusText.isEmpty()) + presence.setStatus(statusText); + // note: not setting priority, according to anti-dicrimination rules;) // for testing diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 3a0c1efb..6cdeb3cf 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.List; @@ -118,6 +119,8 @@ public void setStatus(Status status) { mViewControl.changed(new ViewEvent.StatusChanged()); if (status == Status.CONNECTED) { + String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); + mClient.sendUserPresence(strings.length > 0 ? strings[0] : ""); // send all pending messages for (Chat chat: ChatList.getInstance()) for (OutMessage m : chat.getMessages().getPending()) @@ -582,8 +585,20 @@ public Status getCurrentStatus() { return mCurrentStatus; } - public void sendStatusText() { - mClient.sendInitialPresence(); + public void setStatusText(String newStatus) { + Config conf = Config.getInstance(); + String[] strings = conf.getStringArray(Config.NET_STATUS_LIST); + List stats = new ArrayList<>(Arrays.asList(strings)); + + stats.remove(newStatus); + + stats.add(0, newStatus); + + if (stats.size() > 20) + stats = stats.subList(0, 20); + + conf.setProperty(Config.NET_STATUS_LIST, stats.toArray()); + mClient.sendUserPresence(newStatus); } public Path getFilePath(Attachment attachment) { diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 5e5a2c9f..78599508 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -190,21 +190,7 @@ public void actionPerformed(ActionEvent e) { } private void saveStatus() { - String newStatus = mStatusField.getText(); - - Config conf = Config.getInstance(); - String[] strings = conf.getStringArray(Config.NET_STATUS_LIST); - List stats = new ArrayList<>(Arrays.asList(strings)); - - stats.remove(newStatus); - - stats.add(0, newStatus); - - if (stats.size() > 20) - stats = stats.subList(0, 20); - - conf.setProperty(Config.NET_STATUS_LIST, stats.toArray()); - mView.getControl().sendStatusText(); + mView.getControl().setStatusText(mStatusField.getText()); } } From b18b1e710446c7ef592ff1b688220c0e5307daca Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 17:58:40 +0100 Subject: [PATCH 064/257] view: same fallback avatar color for chat contact and chat --- .../java/org/kontalk/view/AvatarLoader.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index c5e0310a..965dbc95 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -77,23 +77,25 @@ private static class Item { } Item(Chat chat) { + Avatar a = null; + String l = null; + Integer cc = null; if (chat.isGroupChat()) { // nice to have: group picture - avatar = null; - label = chat.getSubject(); + l = chat.getSubject(); } else { Contact[] contacts = chat.getValidContacts(); - if (contacts.length == 0) { - avatar = null; - label = ""; - } else { + if (contacts.length > 0) { Contact c = contacts[0]; - avatar = c.getAvatar().orElse(null); - label = c.getName(); + a = c.getAvatar().orElse(null); + l = c.getName(); + cc = hash(c.getID()); } - } - colorCode = hash(chat.getID()); + + avatar = a; + label = l != null ? l : ""; + colorCode = cc != null ? cc : hash(chat.getID()); } Image createImage() { From 5b1d2059e5b6db98544909c97507f0697ee732a7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 18:24:57 +0100 Subject: [PATCH 065/257] view: better label for unverified messages --- src/main/java/org/kontalk/view/MessageList.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index ded54272..c5a7d2cc 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -514,7 +514,7 @@ else if (enc == Coder.Encryption.DECRYPTED && String verification = Tr.tr("Unknown"); switch (sign) { case NOT: verification = Tr.tr("Not signed"); break; - case SIGNED: verification = Tr.tr("Signed"); break; + case SIGNED: verification = Tr.tr("Not verified"); break; case VERIFIED: verification = Tr.tr("Verified"); break; } sec = encryption + " / " + verification; From c1915f524454fa52b2bb2557369873ed29732c35 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 19:01:34 +0100 Subject: [PATCH 066/257] view: image loader already simplified --- src/main/java/org/kontalk/view/ImageLoader.java | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/kontalk/view/ImageLoader.java b/src/main/java/org/kontalk/view/ImageLoader.java index 63381cce..849ceed7 100644 --- a/src/main/java/org/kontalk/view/ImageLoader.java +++ b/src/main/java/org/kontalk/view/ImageLoader.java @@ -21,7 +21,6 @@ import com.alee.extended.label.WebLinkLabel; import java.awt.Image; import java.awt.image.BufferedImage; -import java.awt.image.ImageObserver; import java.nio.file.Path; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; @@ -44,7 +43,7 @@ static void setImageIconAsync(WebLinkLabel view, Path path) { run.run(); } - private static final class AsyncLoader implements Runnable, ImageObserver { + private static final class AsyncLoader implements Runnable { private final WebLinkLabel view; private final Path path; @@ -61,20 +60,11 @@ public void run() { AttachmentManager.THUMBNAIL_DIM.width, AttachmentManager.THUMBNAIL_DIM.height, false); + if (scaledImage.getWidth(view) == -1) return; - this.setOnEDT(scaledImage); - } - @Override - public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { - // ignore if image is not completely loaded - if ((infoflags & ImageObserver.ALLBITS) == 0) { - return true; - } - - this.setOnEDT(img); - return false; + this.setOnEDT(scaledImage); } private void setOnEDT(final Image image) { From b41e0d0b8e9052b6eda31d2beb26dfad1dceb0c5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 15 Jan 2016 19:03:16 +0100 Subject: [PATCH 067/257] view: avatars for group chats have special color --- .../java/org/kontalk/view/AvatarLoader.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 965dbc95..8fa3426e 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -41,10 +41,13 @@ final class AvatarLoader { private static final int IMG_SIZE = 40; + private static final Color LETTER_COLOR = new Color(255, 255, 255); + private static final Color FALLBACK_COLOR = new Color(220, 220, 220); + private static final Color GROUP_COLOR = new Color(160, 160, 160); + // TODO i18n? private static final String FALLBACK_LETTER = "?"; - private static final Color FALLBACK_COLOR = new Color(220, 220, 220); private static final Map CACHE = new HashMap<>(); @@ -69,20 +72,25 @@ private static class Item { private final Avatar avatar; private final String label; private final int colorCode; + private final boolean group; Item(Contact contact) { avatar = contact.getAvatar().orElse(null); label = contact.getName(); colorCode = hash(contact.getID()); + group = false; } Item(Chat chat) { Avatar a = null; String l = null; Integer cc = null; + if (chat.isGroupChat()) { - // nice to have: group picture + // or use number of contacts here? l = chat.getSubject(); + group = true; + // nice to have: group picture } else { Contact[] contacts = chat.getValidContacts(); if (contacts.length > 0) { @@ -91,6 +99,7 @@ private static class Item { l = c.getName(); cc = hash(c.getID()); } + group = false; } avatar = a; @@ -105,7 +114,7 @@ Image createImage() { return MediaUtils.scaleAsync(img, IMG_SIZE, IMG_SIZE, true); } - return fallback(label, colorCode, IMG_SIZE); + return fallback(label, colorCode, IMG_SIZE, group); } @Override @@ -118,7 +127,8 @@ public boolean equals(Object o) { Item oItem = (Item) o; return ObjectUtils.equals(avatar, oItem.avatar) && - label.equals(oItem.label) && colorCode == oItem.colorCode; + label.equals(oItem.label) && colorCode == oItem.colorCode && + group == oItem.group; } @Override @@ -140,15 +150,17 @@ private static int hash(int x) { } static BufferedImage createFallback(int size) { - return fallback("", 0, size); + return fallback("", 0, size, false); } - private static BufferedImage fallback(String text, int colorCode, int size) { + private static BufferedImage fallback(String text, int colorCode, int size, boolean group) { BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); // color Color color; - if (!text.isEmpty()) { + if (group) { + color = GROUP_COLOR; + } else if (!text.isEmpty()) { int hue = Math.abs(colorCode) % 360; color = Color.getHSBColor(hue / 360.0f, 0.8f, 1); } else { @@ -160,7 +172,7 @@ private static BufferedImage fallback(String text, int colorCode, int size) { graphics.fillRect(0, 0, size, size); // letter - String letter = text.length() > 1 ? + String letter = text.length() >= 1 ? text.substring(0, 1).toUpperCase() : FALLBACK_LETTER; From 001cbced4a4c1e9cc43ccda5fc36266986035cf7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 16 Jan 2016 17:06:43 +0100 Subject: [PATCH 068/257] view: renamed table -> list view --- .../java/org/kontalk/view/ChatListView.java | 4 ++-- .../java/org/kontalk/view/ContactListView.java | 12 ++++++------ .../kontalk/view/{Table.java => ListView.java} | 18 +++++++++--------- src/main/java/org/kontalk/view/MainFrame.java | 8 ++++---- .../java/org/kontalk/view/MessageList.java | 10 +++++----- .../java/org/kontalk/view/SearchPanel.java | 6 +++--- src/main/java/org/kontalk/view/View.java | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) rename src/main/java/org/kontalk/view/{Table.java => ListView.java} (95%) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index c0a0baac..6f97ee5c 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -52,7 +52,7 @@ * Show a brief list of all chats. * @author Alexander Bikadorov {@literal } */ -final class ChatListView extends Table { +final class ChatListView extends ListView { private final ChatList mChatList; private final WebPopupMenu mPopupMenu; @@ -162,7 +162,7 @@ private void deleteSelectedChat() { mView.getControl().deleteChat(chatItem.mValue); } - protected final class ChatItem extends Table.TableItem { + protected final class ChatItem extends ListView.TableItem { private final WebImage mAvatar; private final WebLabel mTitleLabel; diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 7dd09833..9973c044 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -51,7 +51,7 @@ * Display all contact (aka contacts) in a brief list. * @author Alexander Bikadorov {@literal } */ -final class ContactListView extends Table implements Observer { +final class ContactListView extends ListView implements Observer { private final ContactList mContactList; private final ContactPopupMenu mPopupMenu; @@ -125,12 +125,12 @@ private void showPopupMenu(MouseEvent e) { } /** One item in the contact list representing a contact. */ - final class ContactItem extends Table.TableItem { + final class ContactItem extends ListView.TableItem { private final WebImage mAvatar; private final WebLabel mNameLabel; private final WebLabel mStatusLabel; - private Color mBackround; + private Color mBackground; ContactItem(Contact contact) { super(contact); @@ -185,7 +185,7 @@ protected void render(int tableWidth, boolean isSelected) { if (isSelected) this.setBackground(View.BLUE); else - this.setBackground(mBackround); + this.setBackground(mBackground); } @Override @@ -211,12 +211,12 @@ protected void updateOnEDT(Object arg) { // online status Contact.Subscription subStatus = mValue.getSubScription(); - mBackround = mValue.getOnline() == Contact.Online.YES ? View.LIGHT_BLUE: + mBackground = mValue.getOnline() == Contact.Online.YES ? View.LIGHT_BLUE: subStatus == Contact.Subscription.UNSUBSCRIBED || subStatus == Contact.Subscription.PENDING || mValue.isBlocked() ? View.LIGHT_GREY : Color.WHITE; - this.setBackground(mBackround); + this.setBackground(mBackground); ContactListView.this.repaint(); } diff --git a/src/main/java/org/kontalk/view/Table.java b/src/main/java/org/kontalk/view/ListView.java similarity index 95% rename from src/main/java/org/kontalk/view/Table.java rename to src/main/java/org/kontalk/view/ListView.java index 053636e3..87b887e1 100644 --- a/src/main/java/org/kontalk/view/Table.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -61,8 +61,8 @@ * @param the view item in this list * @param the value of one view item */ -abstract class Table.TableItem, V extends Observable> extends WebTable implements Observer { - private static final Logger LOGGER = Logger.getLogger(Table.class.getName()); +abstract class ListView.TableItem, V extends Observable> extends WebTable implements Observer { + private static final Logger LOGGER = Logger.getLogger(ListView.class.getName()); protected final View mView; @@ -80,7 +80,7 @@ abstract class Table.TableItem, V extends Observable> exte // using legacy lib, raw types extend Object @SuppressWarnings("unchecked") - Table(View view, boolean activateTimer) { + ListView(View view, boolean activateTimer) { mView = view; // model @@ -88,7 +88,7 @@ abstract class Table.TableItem, V extends Observable> exte // row sorter needs this @Override public Class getColumnClass(int columnIndex) { - return Table.this.getColumnClass(columnIndex); + return ListView.this.getColumnClass(columnIndex); } }; this.setModel(mModel); @@ -128,9 +128,9 @@ public void mouseDragged(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { - int row = Table.this.rowAtPoint(e.getPoint()); + int row = ListView.this.rowAtPoint(e.getPoint()); if (row >= 0) { - Table.this.editCellAt(row, 0); + ListView.this.editCellAt(row, 0); } } }); @@ -150,7 +150,7 @@ public void mouseExited(MouseEvent e) { TimerTask statusTask = new TimerTask() { @Override public void run() { - Table.this.timerUpdate(); + ListView.this.timerUpdate(); } }; long timerInterval = TimeUnit.SECONDS.toMillis(60); @@ -297,7 +297,7 @@ public void update(Observable o, final Object arg) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - Table.this.updateOnEDT(arg); + ListView.this.updateOnEDT(arg); } }); } @@ -347,7 +347,7 @@ public void run() { // directly to the item, but the behaviour is buggy so we keep this @Override public String getToolTipText(MouseEvent event) { - Table.this.showTooltip(this); + ListView.this.showTooltip(this); return null; } diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 1315e14b..4a3b4047 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -75,8 +75,8 @@ static enum Tab {CHATS, CONTACT}; private final WebToggleButton mAddContactButton; MainFrame(final View view, - Table contactList, - Table chatList, + ListView contactList, + ListView chatList, Component content, WebPanel searchPanel, Component statusBar) { @@ -317,10 +317,10 @@ private void showAboutDialog() { icon); } - private static WebScrollPane createTablePane(final Table table, + private static WebScrollPane createTablePane(final ListView list, String overlayText) { - WebScrollPane scrollPane = new ComponentUtils.ScrollPane(table); + WebScrollPane scrollPane = new ComponentUtils.ScrollPane(list); scrollPane.setDrawBorder(false); // overlay for empty list WebOverlay listOverlayPanel = new WebOverlay(scrollPane); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index c5a7d2cc..d4ec0bc4 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -77,7 +77,7 @@ * View all messages of one chat in a left/right MIM style list. * @author Alexander Bikadorov {@literal } */ -final class MessageList extends Table { +final class MessageList extends ListView { private static final Logger LOGGER = Logger.getLogger(MessageList.class.getName()); private static final Icon PENDING_ICON = Utils.getIcon("ic_msg_pending.png");; @@ -98,7 +98,7 @@ final class MessageList extends Table { mChat = chat; // use custom editor (for mouse events) - this.setDefaultEditor(Table.TableItem.class, new TableEditor()); + this.setDefaultEditor(ListView.TableItem.class, new TableEditor()); //this.setEditable(false); //this.setAutoscrolls(true); @@ -216,7 +216,7 @@ private void setBackground(Chat.ViewSettings s) { * The content is added to a panel inside this panel. For performance * reasons the content is created when the item is rendered in the table */ - final class MessageItem extends Table.TableItem { + final class MessageItem extends ListView.TableItem { private WebPanel mPanel; private WebLabel mFromLabel = null; @@ -654,14 +654,14 @@ public int compareTo(TableItem o) { // needed for correct mouse behaviour for components in items // (and breaks selection behaviour somehow) private class TableEditor extends AbstractCellEditor implements TableCellEditor { - private Table.TableItem mValue; + private ListView.TableItem mValue; @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { - mValue = (Table.TableItem) value; + mValue = (ListView.TableItem) value; return mValue; } @Override diff --git a/src/main/java/org/kontalk/view/SearchPanel.java b/src/main/java/org/kontalk/view/SearchPanel.java index 48247e5d..8e4b0f8d 100644 --- a/src/main/java/org/kontalk/view/SearchPanel.java +++ b/src/main/java/org/kontalk/view/SearchPanel.java @@ -36,7 +36,7 @@ final class SearchPanel extends WebPanel { private final WebTextField mSearchField; - SearchPanel(final Table[] tables, final ChatView chatView) { + SearchPanel(final ListView[] lists, final ChatView chatView) { mSearchField = new WebTextField(); mSearchField.setInputPrompt(Tr.tr("Search…")); mSearchField.getDocument().addDocumentListener(new DocumentListener() { @@ -54,8 +54,8 @@ public void changedUpdate(DocumentEvent e) { } private void filterList() { String searchText = mSearchField.getText().toLowerCase(); - for (Table table : tables) - table.filterItems(searchText); + for (ListView list : lists) + list.filterItems(searchText); chatView.filterCurrentChat(searchText); } }); diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index a6b10929..c23de768 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -124,7 +124,7 @@ private View(ViewControl control) { // search panel mSearchPanel = new SearchPanel( - new Table[]{mContactListView, mChatListView}, + new ListView[]{mContactListView, mChatListView}, mChatView); // status bar From c6c7915ea95813ee2fa8fd9a8a7d4834bf12208a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 17 Jan 2016 16:52:09 +0100 Subject: [PATCH 069/257] view: refactored right click menu in view lists --- .../java/org/kontalk/view/ChatListView.java | 63 ++----- .../org/kontalk/view/ContactListView.java | 178 +++++++----------- src/main/java/org/kontalk/view/ListView.java | 23 +++ .../java/org/kontalk/view/MessageList.java | 128 +++++-------- 4 files changed, 166 insertions(+), 226 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 6f97ee5c..98b624a6 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -29,8 +29,6 @@ import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.util.HashSet; import java.util.Set; import java.util.Timer; @@ -55,7 +53,6 @@ final class ChatListView extends ListView { private final ChatList mChatList; - private final WebPopupMenu mPopupMenu; ChatListView(final View view, ChatList chatList) { super(view, true); @@ -63,19 +60,6 @@ final class ChatListView extends ListView { this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // right click popup menu - mPopupMenu = new WebPopupMenu(); - - WebMenuItem deleteMenuItem = new WebMenuItem(Tr.tr("Delete Chat")); - deleteMenuItem.setToolTipText(Tr.tr("Delete this chat")); - deleteMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - ChatListView.this.deleteSelectedChat(); - } - }); - mPopupMenu.add(deleteMenuItem); - // actions triggered by selection this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @@ -102,25 +86,6 @@ public void valueChanged(ListSelectionEvent e) { } }); - // actions triggered by mouse events - this.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - check(e); - } - @Override - public void mouseReleased(MouseEvent e) { - check(e); - } - private void check(MouseEvent e) { - if (e.isPopupTrigger()) { - int row = ChatListView.this.rowAtPoint(e.getPoint()); - ChatListView.this.setSelectedItem(row); - ChatListView.this.showPopupMenu(e); - } - } - }); - this.updateOnEDT(null); } @@ -145,15 +110,10 @@ void save() { this.getSelectedRow()); } - private void showPopupMenu(MouseEvent e) { - mPopupMenu.show(this, e.getX(), e.getY()); - } - - private void deleteSelectedChat() { - ChatItem t = this.getSelectedItem(); - if (!t.mValue.getMessages().isEmpty()) { + private void deleteChat(ChatItem item) { + if (!item.mValue.getMessages().isEmpty()) { String text = Tr.tr("Permanently delete all messages in this chat?"); - if (t.mValue.isGroupChat() && t.mValue.isValid()) + if (item.mValue.isGroupChat() && item.mValue.isValid()) text += "\n\n"+Tr.tr("You will automatically leave this group."); if (!Utils.confirmDeletion(this, text)) return; @@ -162,6 +122,23 @@ private void deleteSelectedChat() { mView.getControl().deleteChat(chatItem.mValue); } + @Override + protected WebPopupMenu rightClickMenu(ChatItem item) { + WebPopupMenu menu = new WebPopupMenu(); + + WebMenuItem deleteItem = new WebMenuItem(Tr.tr("Delete Chat")); + deleteItem.setToolTipText(Tr.tr("Delete this chat")); + deleteItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + ChatListView.this.deleteChat(ChatListView.this.getSelectedItem()); + } + }); + menu.add(deleteItem); + + return menu; + } + protected final class ChatItem extends ListView.TableItem { private final WebImage mAvatar; diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 9973c044..71b541b1 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -27,7 +27,6 @@ import com.alee.laf.menu.WebPopupMenu; import java.awt.BorderLayout; import java.awt.Color; -import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; @@ -36,7 +35,6 @@ import java.util.Observer; import java.util.Set; import javax.swing.Box; -import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.StringEscapeUtils; @@ -54,19 +52,12 @@ final class ContactListView extends ListView implements Observer { private final ContactList mContactList; - private final ContactPopupMenu mPopupMenu; ContactListView(final View view, ContactList contactList) { super(view, true); mContactList = contactList; - this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - //this.setDragEnabled(true); - - // right click popup menu - mPopupMenu = new ContactPopupMenu(); - // actions triggered by selection this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @Override @@ -89,21 +80,6 @@ public void mouseClicked(MouseEvent e) { mView.showChat(contact); } } - @Override - public void mousePressed(MouseEvent e) { - check(e); - } - @Override - public void mouseReleased(MouseEvent e) { - check(e); - } - private void check(MouseEvent e) { - if (e.isPopupTrigger()) { - int row = ContactListView.this.rowAtPoint(e.getPoint()); - ContactListView.this.setSelectedItem(row); - ContactListView.this.showPopupMenu(e); - } - } }); this.updateOnEDT(null); @@ -119,11 +95,80 @@ protected void updateOnEDT(Object arg) { this.sync(contacts, newItems); } - private void showPopupMenu(MouseEvent e) { - // note: only work when right click does also selection - mPopupMenu.show(this.getSelectedItem(), this, e.getX(), e.getY()); + @Override + protected WebPopupMenu rightClickMenu(ContactItem item) { + WebPopupMenu menu = new WebPopupMenu(); + + WebMenuItem newItem = new WebMenuItem(Tr.tr("New Chat")); + newItem.setToolTipText(Tr.tr("Creates a new chat for this contact")); + newItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().getOrCreateSingleChat( + ContactListView.this.getSelectedItem().mValue); + } + }); + menu.add(newItem); + + WebMenuItem blockItem = new WebMenuItem(Tr.tr("Block Contact")); + blockItem.setToolTipText(Tr.tr("Block all messages from this contact")); + blockItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().sendContactBlocking( + ContactListView.this.getSelectedItem().mValue, true); + } + }); + menu.add(blockItem); + + WebMenuItem unblockItem = new WebMenuItem(Tr.tr("Unblock Contact")); + unblockItem.setToolTipText(Tr.tr("Unblock this contact")); + unblockItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().sendContactBlocking( + ContactListView.this.getSelectedItem().mValue, false); + } + }); + menu.add(unblockItem); + + WebMenuItem deleteItem = new WebMenuItem(Tr.tr("Delete Contact")); + deleteItem.setToolTipText(Tr.tr("Delete this contact")); + deleteItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + String text = Tr.tr("Permanently delete this contact?") + "\n" + + View.REMOVE_CONTACT_NOTE; + if (!Utils.confirmDeletion(ContactListView.this, text)) + return; + mView.getControl().deleteContact( + ContactListView.this.getSelectedItem().mValue); + mView.showNothing(); + } + }); + menu.add(deleteItem); + + // dont allow creation of more than one chat for a contact + newItem.setVisible(!ChatList.getInstance().contains(item.mValue)); + + if (item.mValue.isBlocked()) { + blockItem.setVisible(false); + unblockItem.setVisible(true); + } else { + blockItem.setVisible(true); + unblockItem.setVisible(false); + } + + Control.Status status = mView.getCurrentStatus(); + boolean connected = status == Control.Status.CONNECTED; + blockItem.setEnabled(connected); + unblockItem.setEnabled(connected); + deleteItem.setEnabled(connected); + + return menu; } + /** One item in the contact list representing a contact. */ final class ContactItem extends ListView.TableItem { @@ -226,83 +271,4 @@ public int compareTo(TableItem o) { return Utils.compareContacts(mValue, o.mValue); } } - - private class ContactPopupMenu extends WebPopupMenu { - - ContactItem mItem; - WebMenuItem mNewMenuItem; - WebMenuItem mBlockMenuItem; - WebMenuItem mUnblockMenuItem; - WebMenuItem mDeleteMenuItem; - - ContactPopupMenu() { - mNewMenuItem = new WebMenuItem(Tr.tr("New Chat")); - mNewMenuItem.setToolTipText(Tr.tr("Creates a new chat for this contact")); - mNewMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.getControl().getOrCreateSingleChat(mItem.mValue); - } - }); - this.add(mNewMenuItem); - - mBlockMenuItem = new WebMenuItem(Tr.tr("Block Contact")); - mBlockMenuItem.setToolTipText(Tr.tr("Block all messages from this contact")); - mBlockMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.getControl().sendContactBlocking(mItem.mValue, true); - } - }); - this.add(mBlockMenuItem); - - mUnblockMenuItem = new WebMenuItem(Tr.tr("Unblock Contact")); - mUnblockMenuItem.setToolTipText(Tr.tr("Unblock this contact")); - mUnblockMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.getControl().sendContactBlocking(mItem.mValue, false); - } - }); - this.add(mUnblockMenuItem); - - mDeleteMenuItem = new WebMenuItem(Tr.tr("Delete Contact")); - mDeleteMenuItem.setToolTipText(Tr.tr("Delete this contact")); - mDeleteMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - String text = Tr.tr("Permanently delete this contact?") + "\n" + - View.REMOVE_CONTACT_NOTE; - if (!Utils.confirmDeletion(ContactListView.this, text)) - return; - mView.getControl().deleteContact(mItem.mValue); - mView.showNothing(); - } - }); - this.add(mDeleteMenuItem); - } - - void show(ContactItem item, Component invoker, int x, int y) { - mItem = item; - - // dont allow creation of more than one chat for a contact - mNewMenuItem.setVisible(!ChatList.getInstance().contains(item.mValue)); - - if (mItem.mValue.isBlocked()) { - mBlockMenuItem.setVisible(false); - mUnblockMenuItem.setVisible(true); - } else { - mBlockMenuItem.setVisible(true); - mUnblockMenuItem.setVisible(false); - } - - Control.Status status = ContactListView.this.mView.getCurrentStatus(); - boolean connected = status == Control.Status.CONNECTED; - mBlockMenuItem.setEnabled(connected); - mUnblockMenuItem.setEnabled(connected); - mDeleteMenuItem.setEnabled(connected); - - this.show(invoker, x, y); - } - } } diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index 87b887e1..a16dffc0 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -18,6 +18,7 @@ package org.kontalk.view; +import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.panel.WebPanel; import com.alee.laf.table.WebTable; import com.alee.laf.table.renderers.WebTableCellRenderer; @@ -137,6 +138,21 @@ public void mouseMoved(MouseEvent e) { // actions triggered by mouse events this.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + check(e); + } + @Override + public void mouseReleased(MouseEvent e) { + check(e); + } + private void check(MouseEvent e) { + if (e.isPopupTrigger()) { + int row = ListView.this.rowAtPoint(e.getPoint()); + ListView.this.setSelectedItem(row); + ListView.this.showPopupMenu(e, ListView.this.getSelectedItem()); + } + } @Override public void mouseExited(MouseEvent e) { if (mTip != null) @@ -160,6 +176,13 @@ public void run() { } } + private void showPopupMenu(MouseEvent e, I item) { + WebPopupMenu menu = this.rightClickMenu(item); + menu.show(this, e.getX(), e.getY()); + } + + protected abstract WebPopupMenu rightClickMenu(I item); + protected boolean containsValue(V value) { return mItems.containsKey(value); } diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index d4ec0bc4..188b63fc 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -33,8 +33,6 @@ import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; @@ -97,6 +95,9 @@ final class MessageList extends ListView { mChatView = chatView; mChat = chat; + // disable selection + this.setSelectionModel(new UnselectableListModel()); + // use custom editor (for mouse events) this.setDefaultEditor(ListView.TableItem.class, new TableEditor()); @@ -107,26 +108,6 @@ final class MessageList extends ListView { // hide grid this.setShowGrid(false); - // disable selection - this.setSelectionModel(new UnselectableListModel()); - - // actions triggered by mouse events - this.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - check(e); - } - @Override - public void mouseReleased(MouseEvent e) { - check(e); - } - private void check(MouseEvent e) { - if (e.isPopupTrigger()) { - MessageList.this.showPopupMenu(e); - } - } - }); - this.setBackground(mChat.getViewSettings()); this.setVisible(false); @@ -196,21 +177,51 @@ private void insertMessage(KonMessage message) { mChatView.setScrolling(); } - private void showPopupMenu(MouseEvent e) { - int row = this.rowAtPoint(e.getPoint()); - if (row < 0) - return; - - MessageItem messageView = this.getDisplayedItemAt(row); - WebPopupMenu popupMenu = messageView.getPopupMenu(); - popupMenu.show(this, e.getX(), e.getY()); - } - private void setBackground(Chat.ViewSettings s) { // simply overwrite mBackground = mChatView.createBG(s); } + @Override + protected WebPopupMenu rightClickMenu(MessageItem item) { + WebPopupMenu menu = new WebPopupMenu(); + + final KonMessage m = item.mValue; + if (m instanceof InMessage) { + if (m.isEncrypted()) { + WebMenuItem decryptMenuItem = new WebMenuItem(Tr.tr("Decrypt")); + decryptMenuItem.setToolTipText(Tr.tr("Retry decrypting message")); + decryptMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().decryptAgain((InMessage) m); + } + }); + menu.add(decryptMenuItem); + } + Attachment att = m.getContent().getAttachment().orElse(null); + if (att != null && + att.getFile().toString().isEmpty()) { + WebMenuItem attMenuItem = new WebMenuItem(Tr.tr("Load")); + attMenuItem.setToolTipText(Tr.tr("Retry downloading attachment")); + attMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().downloadAgain((InMessage) m); + } + }); + menu.add(attMenuItem); + } + } + + WebMenuItem cItem = Utils.createCopyMenuItem( + toCopyString(m), + Tr.tr("Copy message content")); + menu.add(cItem); + + return menu; + } + /** * View for one message. * The content is added to a panel inside this panel. For performance @@ -578,51 +589,6 @@ private void updateAttachment() { } } - private WebPopupMenu getPopupMenu() { - WebPopupMenu popupMenu = new WebPopupMenu(); - final KonMessage m = MessageItem.this.mValue; - if (m instanceof InMessage) { - if (m.isEncrypted()) { - WebMenuItem decryptMenuItem = new WebMenuItem(Tr.tr("Decrypt")); - decryptMenuItem.setToolTipText(Tr.tr("Retry decrypting message")); - decryptMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.getControl().decryptAgain((InMessage) m); - } - }); - popupMenu.add(decryptMenuItem); - } - Attachment att = m.getContent().getAttachment().orElse(null); - if (att != null && - att.getFile().toString().isEmpty()) { - WebMenuItem attMenuItem = new WebMenuItem(Tr.tr("Load")); - attMenuItem.setToolTipText(Tr.tr("Retry downloading attachment")); - attMenuItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.getControl().downloadAgain((InMessage) m); - } - }); - popupMenu.add(attMenuItem); - } - } - - WebMenuItem cItem = Utils.createCopyMenuItem( - this.toCopyString(), - Tr.tr("Copy message content")); - popupMenu.add(cItem); - return popupMenu; - } - - private String toCopyString() { - String date = Utils.LONG_DATE_FORMAT.format(mValue.getDate()); - String from = mValue instanceof InMessage ? - getFromString((InMessage) mValue) : - Tr.tr("me"); - return date + " - " + from + " : " + mValue.getContent().getText(); - } - @Override protected boolean contains(String search) { if (mValue.getContent().getText().toLowerCase().contains(search)) @@ -674,6 +640,14 @@ private static String getFromString(InMessage message) { return Utils.displayName(message.getContact(), message.getJID(), 40); } + private static String toCopyString(KonMessage m) { + String date = Utils.LONG_DATE_FORMAT.format(m.getDate()); + String from = m instanceof InMessage ? + getFromString((InMessage) m) : + Tr.tr("me"); + return date + " - " + from + " : " + m.getContent().getText(); + } + private static final WrapEditorKit FIX_WRAP_KIT = new WrapEditorKit(); /** From e080db5adaab06aebe6c039c9474922153a046b2 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 18 Jan 2016 15:02:57 +0100 Subject: [PATCH 070/257] view: new edit icon for editable fields --- .../java/org/kontalk/view/ComponentUtils.java | 5 +++++ src/main/resources/ic_ui_edit.png | Bin 0 -> 410 bytes 2 files changed, 5 insertions(+) create mode 100644 src/main/resources/ic_ui_edit.png diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 78599508..02d840ba 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -18,6 +18,7 @@ package org.kontalk.view; +import com.alee.extended.image.WebImage; import com.alee.extended.label.WebLinkLabel; import com.alee.extended.layout.FormLayout; import com.alee.extended.panel.GroupPanel; @@ -657,6 +658,8 @@ static class EditableTextField extends WebTextField { int columns, final Component focusGainer) { super(new ComponentUtils.TextLimitDocument(maxTextLength), text, columns); + this.setTrailingComponent(new WebImage(Utils.getIcon("ic_ui_edit.png"))); + this.setEditable(editable); this.setFocusable(editable); @@ -690,11 +693,13 @@ private void switchToEditMode() { this.setInputPrompt(text); this.setText(text); this.setDrawBorder(true); + this.getTrailingComponent().setVisible(false); } private void switchToLabelMode() { this.setText(this.labelText()); this.setDrawBorder(false); + this.getTrailingComponent().setVisible(true); } protected String labelText() { diff --git a/src/main/resources/ic_ui_edit.png b/src/main/resources/ic_ui_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..61949394d027df0d9b1ca5cf23360bc991a23559 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^QXtI10wkH8TU>z@OS+@4BLl<6e(pbstUx|flDE4H z!~gdFGy8!&&H|6fVg?3oVGw3ym^DWND9B#o>Fdh=fKgC@MN+sV?G6J2qqL`sV~EA+ zvr}xnof0M5)+;YB(==gk4^x(2)U!ynDX(4O597c7I!F5@h5|(|C9@`RWLri%wQ-u=-w@bje3{Rz=0H2!^2fb)UkD?JI@<%W&y0O3OSK zB_b94z-IFii!1XMDmnk@F%G%-?01A%`QNk}riOVTmcgquJ^@3H!PC{xWt~$(698a0 Bm+k-n literal 0 HcmV?d00001 From 88b6c53b8577eddcf12815a765ae6ac7e596c470 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 18 Jan 2016 15:31:27 +0100 Subject: [PATCH 071/257] view: added "edit contact" option to chat list right click menu --- src/main/java/org/kontalk/view/ChatListView.java | 15 +++++++++++++++ src/main/java/org/kontalk/view/ListView.java | 4 ++++ src/main/java/org/kontalk/view/View.java | 2 ++ 3 files changed, 21 insertions(+) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 98b624a6..048ee255 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -43,6 +43,7 @@ import org.kontalk.model.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.MessageContent.GroupCommand; +import org.kontalk.model.SingleChat; import org.kontalk.util.Tr; import org.kontalk.view.ChatListView.ChatItem; @@ -126,6 +127,20 @@ private void deleteChat(ChatItem item) { protected WebPopupMenu rightClickMenu(ChatItem item) { WebPopupMenu menu = new WebPopupMenu(); + Chat chat = item.mValue; + if (chat instanceof SingleChat) { + final Contact contact = ((SingleChat) chat).getContact(); + WebMenuItem editItem = new WebMenuItem(Tr.tr("Edit Contact")); + editItem.setToolTipText(Tr.tr("Edit contact settings")); + editItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.showContactDetails(contact); + } + }); + menu.add(editItem); + } + WebMenuItem deleteItem = new WebMenuItem(Tr.tr("Delete Chat")); deleteItem.setToolTipText(Tr.tr("Delete this chat")); deleteItem.addActionListener(new ActionListener() { diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index a16dffc0..35c40fe6 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -251,6 +251,10 @@ void setSelectedItem(V value) { protected void setSelectedItem(int i) { if (i >= mModel.getRowCount()) return; + + if (i == this.getSelectedRow()) + return; + this.setSelectedRow(i); } diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index c23de768..3074659c 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -330,6 +330,8 @@ private void selectChat(Chat chat) { } void showContactDetails(Contact contact) { + mMainFrame.selectTab(MainFrame.Tab.CONTACT); + mContactListView.setSelectedItem(contact); mContent.showContact(contact); } From 2e4aa0a67fbd55de6c29691ad815f09f9dd80069 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 18 Jan 2016 16:49:51 +0100 Subject: [PATCH 072/257] view: request contact rename focus when pressing F2 in side lists, finally closes #60 --- src/main/java/org/kontalk/view/ChatListView.java | 14 ++++++++++++++ src/main/java/org/kontalk/view/ContactDetails.java | 4 ++++ .../java/org/kontalk/view/ContactListView.java | 8 ++++++++ src/main/java/org/kontalk/view/Content.java | 6 ++++++ src/main/java/org/kontalk/view/ListView.java | 14 ++++++++++++++ src/main/java/org/kontalk/view/View.java | 11 +++++++++++ 6 files changed, 57 insertions(+) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 048ee255..0db95430 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -42,6 +42,7 @@ import org.kontalk.model.Chat.KonChatState; import org.kontalk.model.ChatList; import org.kontalk.model.Contact; +import org.kontalk.model.GroupChat; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.SingleChat; import org.kontalk.util.Tr; @@ -154,6 +155,19 @@ public void actionPerformed(ActionEvent event) { return menu; } + @Override + protected void onRenameEvent() { + Chat chat = this.getSelectedValue().orElse(null); + if (chat instanceof SingleChat) { + mView.requestRenameFocus(((SingleChat) chat).getContact()); + return; + } + + if (chat instanceof GroupChat) { + // TODO + } + } + protected final class ChatItem extends ListView.TableItem { private final WebImage mAvatar; diff --git a/src/main/java/org/kontalk/view/ContactDetails.java b/src/main/java/org/kontalk/view/ContactDetails.java index 3840851c..7b5fc9f9 100644 --- a/src/main/java/org/kontalk/view/ContactDetails.java +++ b/src/main/java/org/kontalk/view/ContactDetails.java @@ -206,6 +206,10 @@ public void paintComponent(Graphics g) { this.add(gradientPanel, BorderLayout.CENTER); } + void setRenameFocus() { + mNameField.requestFocusInWindow(); + } + @Override public void update(Observable o, final Object arg) { if (SwingUtilities.isEventDispatchThread()) { diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 71b541b1..ac254c3e 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -168,6 +168,14 @@ public void actionPerformed(ActionEvent event) { return menu; } + @Override + protected void onRenameEvent() { + Contact contact = this.getSelectedValue().orElse(null); + if (contact == null) + return; + + mView.requestRenameFocus(contact); + } /** One item in the contact list representing a contact. */ final class ContactItem extends ListView.TableItem { diff --git a/src/main/java/org/kontalk/view/Content.java b/src/main/java/org/kontalk/view/Content.java index 499794e1..31abbe4b 100644 --- a/src/main/java/org/kontalk/view/Content.java +++ b/src/main/java/org/kontalk/view/Content.java @@ -77,4 +77,10 @@ private void show(Component comp) { mCurrent = comp; } + + void requestRenameFocus() { + if (mCurrent instanceof ContactDetails) { + ((ContactDetails) mCurrent).setRenameFocus(); + } + } } diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index 35c40fe6..3f0ccf2a 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -29,6 +29,8 @@ import java.awt.Component; import java.awt.Point; import java.awt.Rectangle; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; @@ -160,6 +162,16 @@ public void mouseExited(MouseEvent e) { } }); + // actions triggered by key events + this.addKeyListener(new KeyAdapter() { + @Override + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_F2){ + ListView.this.onRenameEvent(); + } + } + }); + if (activateTimer) { mTimer = new Timer(); // update periodically items to be up-to-date with 'last seen' text @@ -331,6 +343,8 @@ public void run() { abstract protected void updateOnEDT(Object arg); + protected void onRenameEvent() {} + abstract class TableItem extends WebPanel implements Observer, Comparable { protected final V mValue; diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 3074659c..3137b6e0 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -330,11 +330,22 @@ private void selectChat(Chat chat) { } void showContactDetails(Contact contact) { + if (contact.isDeleted()) + return; + mMainFrame.selectTab(MainFrame.Tab.CONTACT); mContactListView.setSelectedItem(contact); mContent.showContact(contact); } + void requestRenameFocus(Contact contact) { + if (contact.isDeleted()) + return; + + this.showContactDetails(contact); + mContent.requestRenameFocus(); + } + void showChat(Chat chat) { if (mMainFrame.getCurrentTab() != MainFrame.Tab.CHATS) return; From 238e91c60f306cffa817e1c62200fbde139e42cc Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Tue, 19 Jan 2016 21:11:42 +0100 Subject: [PATCH 073/257] set up travis --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9421d903 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: java + +jdk: + - oraclejdk8 + +before_install: + - git submodule update --init + From af0c2397c90cd5e35c1c994f4d11300fcdf7a05d Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Tue, 19 Jan 2016 21:40:49 +0100 Subject: [PATCH 074/257] Add travis build status See #62 --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2368fbd1..a9470788 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ kontalk-java-client =================== +[![Build Status](https://travis-ci.org/kontalk/desktopclient-java.svg?branch=master)](https://travis-ci.org/kontalk/desktopclient-java) + A platform independent Java client for Kontalk (http://www.kontalk.org). Includes connectivity to the Jabber network! The desktop client uses your existing Kontalk account from the [Android client](https://github.com/kontalk/androidclient/blob/master/README.md#kontalk-official-android-client). Instructions for exporting the key [here](https://github.com/kontalk/androidclient/wiki/Export-personal-key-to-another-device). From 11d3b48812a18f1159795df88cc23d8f5623161a Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 20 Jan 2016 20:42:06 +0100 Subject: [PATCH 075/257] ignore intelliJ to avoid merge conflicts from new committers --- .gitignore | 8 +- .idea/.name | 1 - .idea/compiler.xml | 22 ----- .idea/copyright/profiles_settings.xml | 3 - .idea/misc.xml | 57 ------------ .idea/modules.xml | 8 -- .idea/uiDesigner.xml | 124 -------------------------- .idea/vcs.xml | 6 -- 8 files changed, 2 insertions(+), 227 deletions(-) delete mode 100644 .idea/.name delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/copyright/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/uiDesigner.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index f5980143..b757f77c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,12 +14,8 @@ /.nb-gradle/ # IntelliJ Idea -/.idea/workspace.xml -/.idea/tasks.xml -/.idea/gradle.xml -/.idea/libraries/ -/.idea/inspectionProfiles/ -/*.iml +.idea +*.iml # Package Files # #*.jar diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index b1f2454c..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -kontalk_desktop_java \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 96cc43ef..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf33..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 61df1558..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 3b2f3ba1..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index e96534fb..00000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 7bc759c1218b144614aa1fd2ccdbb0659a14dd6a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 18 Jan 2016 17:08:54 +0100 Subject: [PATCH 076/257] (auto)update translation strings --- src/main/resources/i18n/strings.properties | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 21cace71..6eccb8c1 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -183,7 +183,6 @@ s_RX28 = Secure s_9CW2 = Encrypted s_5VTT = Decrypted s_PBTX = Not signed -s_AUZG = Signed s_SIR1 = Verified s_WN8A = Supported files s_CQEO = File @@ -222,9 +221,19 @@ s_OOS3 = Connecting… s_0WD8 = Disconnecting… s_Z1GT = Can't load all keyfiles from archive. s_MMBD = Search… -s_ZZWW = Send chat activity (typing,…) to other user s_PC1G = is writing… s_KRQ6 = Enter password… s_2R2P = loading… s_307W = downloading… s_4WC0 = The server certificate could not be validated. +s_68LN = Request +s_7MDX = Request status authorization from contact +s_OTEU = Automatically grant authorization +s_MFZY = Automatically grant online status authorization requests from other users +s_FAS6 = Send chat activity (typing,…) to other users +s_29CW = Edit Contact +s_AGYA = Edit contact settings +s_MNIJ = Waiting... +s_0WUC = Not verified +s_9S4L = Authorization request +s_FDTC = When accepting, this contact will be able to see your online status. From fea291821323a26478d17fe3dd2ad8cffc4ce1b6 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 21 Jan 2016 14:39:21 +0100 Subject: [PATCH 077/257] minor updates to gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b757f77c..3879d3ef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ /kontalk.db /kontalk.properties *.pgp +*.asc +*.crt /win_installer/*.exe # Gradle @@ -14,7 +16,7 @@ /.nb-gradle/ # IntelliJ Idea -.idea +.idea/ *.iml # Package Files # From f56d597defc873d639514e1ded87d5bd82987ffc Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 21 Jan 2016 17:01:22 +0100 Subject: [PATCH 078/257] crypto: fix X.509 bridge certificate creation with legacy key --- .../java/org/kontalk/crypto/PersonalKey.java | 56 ++++++++++++------- .../java/org/kontalk/crypto/X509Bridge.java | 39 ++++++++++--- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/PersonalKey.java b/src/main/java/org/kontalk/crypto/PersonalKey.java index 6658e8a6..72f1bf17 100644 --- a/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -34,8 +34,10 @@ import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; @@ -59,16 +61,20 @@ public final class PersonalKey { private final PGPKeyPair mEncryptKey; /** X.509 bridge certificate. */ private final X509Certificate mBridgeCert; + /** Primary user ID. */ + private final String mUID; private PersonalKey(PGPKeyPair authKP, PGPKeyPair signKP, PGPKeyPair encryptKP, - X509Certificate bridgeCert) throws PGPException { + X509Certificate bridgeCert, + String uid) throws PGPException { mAuthKey = authKP.getPublicKey(); mLoginKey = PGPUtils.convertPrivateKey(authKP.getPrivateKey()); mSignKey = signKP; mEncryptKey = encryptKP; mBridgeCert = bridgeCert; + mUID = uid; } PGPPrivateKey getPrivateEncryptionKey() { @@ -97,10 +103,7 @@ public PrivateKey getServerLoginKey() { /** Returns the first user ID in the key. */ public String getUserId() { - Iterator uidIt = mAuthKey.getUserIDs(); - if (!uidIt.hasNext()) - throw new IllegalStateException("no UID in personal key"); - return (String) uidIt.next(); + return mUID; } public String getFingerprint() { @@ -166,27 +169,38 @@ public static PersonalKey load(byte[] privateKeyData, PGPKeyPair signKeyPair = PGPUtils.decrypt(signKey, decryptor); PGPKeyPair encryptKeyPair = PGPUtils.decrypt(encrKey, decryptor); + // user ID + Iterator uidIt = authKey.getUserIDs(); + if (!uidIt.hasNext()) + throw new KonException(KonException.Error.LOAD_KEY, + new PGPException("no UID in key")); + String uid = (String) uidIt.next(); + // X.509 bridge certificate - X509Certificate bridgeCert = bridgeCertData == null ? - createX509Certificate(authKeyPair, signKeyPair, encryptKeyPair) : - PGPUtils.loadX509Cert(bridgeCertData); + X509Certificate bridgeCert; + if (bridgeCertData != null) { + bridgeCert = PGPUtils.loadX509Cert(bridgeCertData); + } else { + // public key ring + ByteArrayOutputStream out = new ByteArrayOutputStream(); + authKeyPair.getPublicKey().encode(out); + signKeyPair.getPublicKey().encode(out); + encryptKeyPair.getPublicKey().encode(out); + byte[] publicKeyRingData = out.toByteArray(); + PGPPublicKeyRing pubKeyRing = new BcPGPPublicKeyRing(publicKeyRingData); + + // re-create cert + bridgeCert = createX509Certificate(authKeyPair, pubKeyRing); + } - return new PersonalKey(authKeyPair, signKeyPair, encryptKeyPair, bridgeCert); + return new PersonalKey(authKeyPair, signKeyPair, encryptKeyPair, bridgeCert, uid); } - private static X509Certificate createX509Certificate( - PGPKeyPair authKP, - PGPKeyPair signKP, - PGPKeyPair encryptKP) throws IOException, KonException { - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - authKP.getPublicKey().encode(out); - signKP.getPublicKey().encode(out); - encryptKP.getPublicKey().encode(out); - byte[] publicKeyRingData = out.toByteArray(); - + private static X509Certificate createX509Certificate(PGPKeyPair keyPair, + PGPPublicKeyRing keyRing) + throws KonException { try { - return X509Bridge.createCertificate(authKP, publicKeyRingData); + return X509Bridge.createCertificate(keyPair, keyRing.getEncoded()); } catch (InvalidKeyException | IllegalStateException | NoSuchAlgorithmException | SignatureException | CertificateException | NoSuchProviderException | PGPException | IOException | OperatorCreationException ex) { diff --git a/src/main/java/org/kontalk/crypto/X509Bridge.java b/src/main/java/org/kontalk/crypto/X509Bridge.java index 59eacde0..21df84e7 100644 --- a/src/main/java/org/kontalk/crypto/X509Bridge.java +++ b/src/main/java/org/kontalk/crypto/X509Bridge.java @@ -33,11 +33,15 @@ import java.security.cert.X509Certificate; import java.util.Date; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Object; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.DERBitString; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.misc.MiscObjectIdentifiers; import org.bouncycastle.asn1.misc.NetscapeCertType; @@ -108,9 +112,14 @@ public static X509Certificate createCertificate(PGPKeyPair keyPair, byte[] publi PGPPublicKey publicKey = keyPair.getPublicKey(); + List xmppAddrs = new LinkedList<>(); for (@SuppressWarnings("unchecked") Iterator it = publicKey.getUserIDs(); it.hasNext();) { - Object attrib = it.next(); - x500NameBuilder.addRDN(BCStyle.CN, attrib.toString()); + String attrib = it.next().toString(); + x500NameBuilder.addRDN(BCStyle.CN, attrib); + // extract email for the subjectAltName + String email = PGPUtils.parseUID(attrib)[2]; + if (!email.isEmpty()) + xmppAddrs.add(email); } X500Name x509name = x500NameBuilder.build(); @@ -135,7 +144,7 @@ public static X509Certificate createCertificate(PGPKeyPair keyPair, byte[] publi PGPUtils.convertPrivateKey(keyPair.getPrivateKey()), x509name, creationTime, validTo, - null, + xmppAddrs, publicKeyRingData); } @@ -165,7 +174,7 @@ public static X509Certificate createCertificate(PGPKeyPair keyPair, byte[] publi */ private static X509Certificate createCertificate(PublicKey pubKey, PrivateKey privKey, X500Name subject, - Date startDate, Date endDate, String subjectAltName, byte[] publicKeyData) + Date startDate, Date endDate, List subjectAltNames, byte[] publicKeyData) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, SignatureException, CertificateException, NoSuchProviderException, IOException, OperatorCreationException { @@ -269,11 +278,14 @@ false, new NetscapeCertType( /* * Adds the subject alternative-name extension. */ - if (subjectAltName != null) { - GeneralNames subjectAltNames = new GeneralNames(new GeneralName( - GeneralName.otherName, subjectAltName)); + if (subjectAltNames != null && subjectAltNames.size() > 0) { + GeneralName[] names = new GeneralName[subjectAltNames.size()]; + for (int i = 0; i < names.length; i++) + names[i] = new GeneralName(GeneralName.otherName, + new XmppAddrIdentifier(subjectAltNames.get(i))); + certBuilder.addExtension(Extension.subjectAlternativeName, - false, subjectAltNames); + false, new GeneralNames(names)); } /* @@ -327,7 +339,16 @@ public DERBitString getPublicKeyData() public ASN1Primitive toASN1Primitive() { return keyData; } + } - } + private static class XmppAddrIdentifier extends DLSequence { + static final ASN1ObjectIdentifier OID = new ASN1ObjectIdentifier("1.3.6.1.5.5.7.8.5"); + XmppAddrIdentifier(String jid) { + super(new ASN1Encodable[] { + OID, + new DERUTF8String(jid) + }); + } + } } From 37ee5ed39e6aaa6004a87cf4dfc04585c4f7e2ac Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Jan 2016 17:52:25 +0100 Subject: [PATCH 079/257] model: wrapped password and JID inside account --- .../kontalk/client/KonConnectionListener.java | 4 ++-- src/main/java/org/kontalk/misc/JID.java | 9 ++------- src/main/java/org/kontalk/model/Account.java | 20 +++++++++++++++---- .../java/org/kontalk/model/ContactList.java | 2 +- src/main/java/org/kontalk/system/Control.java | 2 +- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonConnectionListener.java b/src/main/java/org/kontalk/client/KonConnectionListener.java index 1ae6d648..fa02fbbf 100644 --- a/src/main/java/org/kontalk/client/KonConnectionListener.java +++ b/src/main/java/org/kontalk/client/KonConnectionListener.java @@ -24,7 +24,7 @@ import org.jivesoftware.smack.XMPPConnection; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; -import org.kontalk.system.Config; +import org.kontalk.model.Account; import org.kontalk.system.Control; /** @@ -51,7 +51,7 @@ public void connected(XMPPConnection connection) { public void authenticated(XMPPConnection connection, boolean resumed) { JID jid = JID.bare(connection.getUser()); LOGGER.info("as "+jid); - Config.getInstance().setProperty(Config.ACC_JID, jid.string()); + Account.getInstance().setJID(jid); } @Override diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 694a93f0..21e53868 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -21,7 +21,7 @@ import java.util.Objects; import org.apache.commons.lang.StringUtils; import org.jxmpp.util.XmppStringUtils; -import org.kontalk.system.Config; +import org.kontalk.model.Account; /** * A Jabber ID (the address of an XMPP entity). Immutable. @@ -68,7 +68,7 @@ public boolean isFull() { public boolean isMe() { return this.isValid() && - this.equals(JID.me()); + this.equals(Account.getInstance().getUserJID()); } public JID toBare() { @@ -126,9 +126,4 @@ public static JID bare(String local, String domain) { public static JID deleted(int id) { return new JID("", Integer.toString(id), ""); } - - public static JID me() { - return JID.bare(Config.getInstance().getString(Config.ACC_JID)); - } - } diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index a237fc9f..da635aa6 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -40,10 +40,12 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; +import org.kontalk.misc.JID; import org.kontalk.system.Config; /** * The user account. There can only be one. + * * @author Alexander Bikadorov {@literal } */ public final class Account { @@ -88,10 +90,8 @@ public PersonalKey load(char[] password) throws KonException { public void setAccount(byte[] privateKeyData, char[] password) throws KonException { // try to load key PersonalKey key; - byte[] encodedPrivateKey; try { - encodedPrivateKey = privateKeyData; - key = PersonalKey.load(encodedPrivateKey, password); + key = PersonalKey.load(privateKeyData, password); } catch (PGPException | IOException | CertificateException | NoSuchProviderException ex) { LOGGER.log(Level.WARNING, "can't import personal key", ex); @@ -107,7 +107,7 @@ public void setAccount(byte[] privateKeyData, char[] password) throws KonExcepti throw new KonException(KonException.Error.IMPORT_KEY, ex); } this.writeBytesToFile(bridgeCertData, BRIDGE_CERT_FILENAME, false); - this.writePrivateKey(encodedPrivateKey, password, new char[0]); + this.writePrivateKey(privateKeyData, password, new char[0]); // success! use the new key mKey = key; @@ -120,11 +120,23 @@ public void setAccount(byte[] privateKeyData, char[] password) throws KonExcepti LOGGER.info("new account, temporary JID: "+address); } + public char[] getPassword() { + return Config.getInstance().getString(Config.ACC_PASS).toCharArray(); + } + public void setPassword(char[] oldPassword, char[] newPassword) throws KonException { byte[] privateKeyData = this.readFile(PRIVATE_KEY_FILENAME, true); this.writePrivateKey(privateKeyData, oldPassword, newPassword); } + public void setJID(JID jid) { + Config.getInstance().setProperty(Config.ACC_JID, jid.string()); + } + + public JID getUserJID() { + return JID.bare(Config.getInstance().getString(Config.ACC_JID)); + } + private void writePrivateKey(byte[] privateKeyData, char[] oldPassword, char[] newPassword) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 7424541f..47074f23 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -117,7 +117,7 @@ public Optional get(JID jid) { * if not yet in the list. */ public Optional getMe() { - JID myJID = JID.me(); + JID myJID = Account.getInstance().getUserJID(); Contact contact = this.get(myJID).orElse(null); if (contact == null) return Optional.empty(); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 6cdeb3cf..05aa5ed5 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -772,7 +772,7 @@ private PersonalKey keyOrNull(char[] password) { return null; } - password = Config.getInstance().getString(Config.ACC_PASS).toCharArray(); + password = account.getPassword(); } try { From bed00ada7dfbf96a9f73370ee80c0a10e803a178 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Jan 2016 16:32:28 +0100 Subject: [PATCH 080/257] unified end-of-line --- src/main/java/org/kontalk/util/EncodingUtils.java | 2 ++ src/main/java/org/kontalk/view/ChatView.java | 3 ++- src/main/java/org/kontalk/view/Utils.java | 3 ++- src/main/java/org/kontalk/view/View.java | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/util/EncodingUtils.java b/src/main/java/org/kontalk/util/EncodingUtils.java index 07d6e7a2..61887c88 100644 --- a/src/main/java/org/kontalk/util/EncodingUtils.java +++ b/src/main/java/org/kontalk/util/EncodingUtils.java @@ -26,6 +26,8 @@ public final class EncodingUtils { + public static final String EOL = System.getProperty("line.separator"); + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); private EncodingUtils() { throw new AssertionError(); } diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index e36477d0..d1293d5e 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -76,6 +76,7 @@ import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; import org.kontalk.system.Config; +import org.kontalk.util.EncodingUtils; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; import static org.kontalk.view.View.MARGIN_SMALL; @@ -348,7 +349,7 @@ public void keyPressed(KeyEvent e) { if (enterSends && e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK) { e.consume(); - mSendTextArea.append(System.getProperty("line.separator")); + mSendTextArea.append(EncodingUtils.EOL); } if (enterSends && e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiers() == 0) { diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index f0564f54..7f98fa50 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -59,6 +59,7 @@ import org.kontalk.misc.KonException; import org.kontalk.model.Chat; import org.kontalk.model.Contact; +import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; import org.ocpsoft.prettytime.PrettyTime; @@ -258,7 +259,7 @@ static String lastSeen(Contact contact, boolean pretty, boolean pre) { } static String getErrorText(KonException ex) { - String eol = " " + System.getProperty("line.separator"); + String eol = " " + EncodingUtils.EOL; String errorText; switch (ex.getError()) { case IMPORT_ARCHIVE: diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 3137b6e0..d50ff9d0 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -50,6 +50,7 @@ import org.kontalk.model.ContactList; import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; +import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; /** @@ -427,7 +428,7 @@ public static void showWrongJavaVersionDialog() { if (jVersion.length() >= 3) jVersion = jVersion.substring(2, 3); String errorText = Tr.tr("The installed Java version is too old")+": " + jVersion; - errorText += System.getProperty("line.separator"); + errorText += EncodingUtils.EOL; errorText += Tr.tr("Please install Java 8."); WebOptionPane.showMessageDialog(null, errorText, From 11c2e51d433165975d1d4aa2e0d2a131b2daa475 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Jan 2016 16:34:48 +0100 Subject: [PATCH 081/257] added arg parsing + set custom app directory --- build.gradle | 1 + src/main/java/org/kontalk/Kontalk.java | 58 ++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 7301b3e6..aea514ae 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { compile group: 'org.bouncycastle', name: 'bcpg-jdk15on', version: "$bcVersion" compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: "$bcVersion" compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: "$bcVersion" + compile group: 'commons-cli', name: 'commons-cli', version: "1.3.1" compile group: 'commons-codec', name: 'commons-codec', version: "1.10" compile group: 'commons-configuration', name: 'commons-configuration', version: "1.10" compile group: 'commons-io', name: 'commons-io', version: "2.4" diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 3280312f..f01292cd 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.logging.ConsoleHandler; @@ -31,6 +32,13 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; import org.apache.commons.lang.SystemUtils; import org.kontalk.crypto.PGPUtils; import org.kontalk.model.ChatList; @@ -38,6 +46,7 @@ import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; import org.kontalk.util.CryptoUtils; +import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; import org.kontalk.view.View; @@ -59,7 +68,7 @@ public final class Kontalk { } Kontalk(Path appDir) { - APP_DIR = appDir; + APP_DIR = appDir.toAbsolutePath(); } private void start() { @@ -88,7 +97,12 @@ private void start() { // create app directory boolean created = APP_DIR.toFile().mkdirs(); if (created) - LOGGER.info("created application directory"); + LOGGER.info("created application directory: "+APP_DIR); + + if (!Files.isWritable(APP_DIR)) { + LOGGER.severe("invalid app directory: "+APP_DIR); + return; + } // logging Logger logger = Logger.getLogger(""); @@ -169,7 +183,45 @@ public static void exit() { public static void main(String[] args) { LOGGER.setLevel(Level.ALL); - Kontalk app = new Kontalk(); + // parse args, i18n? + Options options = new Options(); + options.addOption("h", "help", false, "show this help message"); + options.addOption(Option.builder("d") + //.argName("app_dir") + .hasArg() + .desc("set custom configuration directory") + .longOpt("appdir") + .build() + ); + + CommandLineParser parser = new DefaultParser(); + CommandLine cmd; + try { + cmd = parser.parse(options, args); + } catch (ParseException e) { + showHelp(options); + return; + } + if (cmd.hasOption("h")) { + showHelp(options); + return; + } + + String appDir = cmd.getOptionValue("d", ""); + + Kontalk app = appDir.isEmpty() ? + new Kontalk() : + new Kontalk(Paths.get(appDir)); app.start(); } + + private static void showHelp(Options options) { + HelpFormatter formatter = new HelpFormatter(); + String eol = EncodingUtils.EOL; + formatter.printHelp("java -jar [kontalk_jar]", + eol + "Kontalk Java Desktop Client" + eol, + options, + "", + true); + } } From 1cf6db2f09ccb479187facb29d7057b24eb097a1 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Jan 2016 18:24:33 +0100 Subject: [PATCH 082/257] added command line option: run without GUI + handle shutdown signals --- src/main/java/org/kontalk/Kontalk.java | 36 +++++++++++-------- src/main/java/org/kontalk/system/Control.java | 13 +++++-- src/main/java/org/kontalk/view/View.java | 2 +- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index f01292cd..0e195827 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -71,7 +71,7 @@ public final class Kontalk { APP_DIR = appDir.toAbsolutePath(); } - private void start() { + void start(boolean ui) { // check if already running try { InetAddress addr = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); @@ -132,20 +132,26 @@ private void start() { // register provider PGPUtils.registerProvider(); - ViewControl control = Control.create(); + final ViewControl control = Control.create(); - View view = View.create(control).orElse(null); - if (view == null) { - control.shutDown(); - return; // never reached - } + // handle shutdown signals + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + // NOTE: logging does not work here anymore already + control.shutDown(false); + System.out.println("shutdown finished"); + } + }); + + View view = ui ? View.create(control).orElse(null) : null; try { // do now to test if successful Database.initialize(); } catch (KonException ex) { LOGGER.log(Level.SEVERE, "can't initialize database", ex); - control.shutDown(); + control.shutDown(true); return; // never reached } @@ -153,7 +159,8 @@ private void start() { ContactList.getInstance().load(); ChatList.getInstance().load(); - view.init(); + if (view != null) + view.init(); control.launch(); } @@ -165,7 +172,7 @@ public static Path appDir() { return APP_DIR; } - public static void exit() { + public static void removeLock() { if (RUN_LOCK != null) { try { RUN_LOCK.close(); @@ -173,8 +180,6 @@ public static void exit() { LOGGER.log(Level.WARNING, "can't close run socket", ex); } } - LOGGER.info("exit"); - System.exit(0); } /** @@ -187,12 +192,13 @@ public static void main(String[] args) { Options options = new Options(); options.addOption("h", "help", false, "show this help message"); options.addOption(Option.builder("d") - //.argName("app_dir") + .argName("app_dir") .hasArg() + .longOpt("app-dir") .desc("set custom configuration directory") - .longOpt("appdir") .build() ); + options.addOption("c", "no-gui", false, "run without user interface"); CommandLineParser parser = new DefaultParser(); CommandLine cmd; @@ -212,7 +218,7 @@ public static void main(String[] args) { Kontalk app = appDir.isEmpty() ? new Kontalk() : new Kontalk(Paths.get(appDir)); - app.start(); + app.start(!cmd.hasOption("c")); } private static void showHelp(Options options) { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 05aa5ed5..ddfaba8f 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -547,7 +547,11 @@ public void launch() { this.connect(); } - public void shutDown() { + public void shutDown(boolean exit) { + if (mCurrentStatus == Status.SHUTTING_DOWN) + // we were already here + return; + this.disconnect(); LOGGER.info("Shutting down..."); mCurrentStatus = Status.SHUTTING_DOWN; @@ -558,8 +562,11 @@ public void shutDown() { // ignore } Config.getInstance().saveToFile(); - - Kontalk.exit(); + Kontalk.removeLock(); + if (exit) { + LOGGER.info("exit"); + System.exit(0); + } } public void connect() { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index d50ff9d0..9ca8f6e2 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -316,7 +316,7 @@ ViewControl getControl() { void callShutDown() { // trigger save if contact details are shown mContent.showNothing(); - mControl.shutDown(); + mControl.shutDown(true); } /* view internal */ From 9564e23d77e0f9cbc447fc63558c0c02df663e0c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Jan 2016 18:38:08 +0100 Subject: [PATCH 083/257] view: reworked UI start --- src/main/java/org/kontalk/view/View.java | 44 +++++++++++------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 9ca8f6e2..a2afda76 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -33,15 +33,15 @@ import java.util.Observer; import java.util.Optional; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; -import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JDialog; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import java.awt.BorderLayout; import java.awt.Dimension; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; import org.kontalk.system.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Chat; @@ -397,30 +397,28 @@ void updateTray() { /* static */ public static Optional create(final ViewControl control) { - Optional optView = invokeAndWait(new Callable() { - @Override - public View call() throws Exception { - return new View(control); - } - }); - if(!optView.isPresent()) { - LOGGER.log(Level.SEVERE, "can't start view"); - return optView; - } - control.addObserver(optView.get()); - return optView; - } - - private static Optional invokeAndWait(Callable callable) { + View view; try { - FutureTask task = new FutureTask<>(callable); - SwingUtilities.invokeLater(task); - // blocking - return Optional.of(task.get()); + view = invokeAndWait(new Callable() { + @Override + public View call() throws Exception { + return new View(control); + } + }); } catch (ExecutionException | InterruptedException ex) { - LOGGER.log(Level.WARNING, "can't execute task", ex); + LOGGER.log(Level.WARNING, "can't start view", ex); + return Optional.empty(); } - return Optional.empty(); + control.addObserver(view); + return Optional.of(view); + } + + private static T invokeAndWait(Callable callable) + throws InterruptedException, ExecutionException { + FutureTask task = new FutureTask<>(callable); + SwingUtilities.invokeLater(task); + // blocking + return task.get(); } public static void showWrongJavaVersionDialog() { From 90bdbbb43d461ba12b026383ccd996b7b809bf2e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 18:55:41 +0100 Subject: [PATCH 084/257] client: send user avatar stub --- .../java/org/kontalk/client/AvatarSender.java | 76 +++++++++++++++++++ src/main/java/org/kontalk/client/Client.java | 10 +++ 2 files changed, 86 insertions(+) create mode 100644 src/main/java/org/kontalk/client/AvatarSender.java diff --git a/src/main/java/org/kontalk/client/AvatarSender.java b/src/main/java/org/kontalk/client/AvatarSender.java new file mode 100644 index 00000000..b06ed4d1 --- /dev/null +++ b/src/main/java/org/kontalk/client/AvatarSender.java @@ -0,0 +1,76 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.pubsub.LeafNode; +import org.jivesoftware.smackx.pubsub.PayloadItem; +import org.jivesoftware.smackx.pubsub.PubSubManager; + + +/** + * + * Note: Smacks PEPManager is not in a usable state. + * @author Alexander Bikadorov {@literal } + */ +final class AvatarSender { + private static final Logger LOGGER = Logger.getLogger(AvatarSender.class.getName()); + + private static final String DATA_NODE = "urn:xmpp:avatar:data"; + + private final PubSubManager mPubSubManager; + + AvatarSender(PubSubManager m) { + this.mPubSubManager = m; + } + + // TODO beta.kontalk.net does not support this, untested + void publish(String id, byte[] data) { + + LeafNode node; + try { + node = mPubSubManager.createNode(DATA_NODE); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't create node", ex); + return; + } + + PayloadItem item = new PayloadItem<>(id, + new AvatarDataExtension(data)); + try { + // blocking + node.send(item); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't send item", ex); + return; + } + + // publish meta data... + } + +} + + diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 331d21e6..c97bc814 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -45,6 +45,7 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.pubsub.PubSubManager; import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; @@ -436,6 +437,15 @@ public void requestAvatar(JID jid, String id) { mAvatarSendReceiver.requestAndListen(jid, id); } + public void publishAvatar(String id, byte[] data) { + if (mConn == null) + return; + + // TODO + PubSubManager pubSubManager = new PubSubManager(mConn, mConn.getServiceName()); + new AvatarSender(pubSubManager).publish(id, data); + } + @Override public void run() { while (true) { From 97848b42cc7b52b00f1ad2f508bf675da05a8879 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 19:09:56 +0100 Subject: [PATCH 085/257] model: new user avatar class --- .../java/org/kontalk/model/UserAvatar.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/main/java/org/kontalk/model/UserAvatar.java diff --git a/src/main/java/org/kontalk/model/UserAvatar.java b/src/main/java/org/kontalk/model/UserAvatar.java new file mode 100644 index 00000000..5a091b83 --- /dev/null +++ b/src/main/java/org/kontalk/model/UserAvatar.java @@ -0,0 +1,76 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.model; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.imageio.ImageIO; + +/** + * TODO new class here or include in control? + * @author Alexander Bikadorov {@literal } + */ +public final class UserAvatar { + private static final Logger LOGGER = Logger.getLogger(UserAvatar.class.getName()); + + public static final int SIZE = 150; + + private static final String FILENAME = "avatar"; + public static final String EXT = "jpg"; + + private final File mFile; + + private BufferedImage mAvatar = null; + + public UserAvatar(Path appDir) { + mFile = appDir.resolve(FILENAME + "." + EXT).toFile(); + } + + public BufferedImage get() { + if (mAvatar != null) + return mAvatar; + + if (!mFile.exists()) { + mAvatar = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + } else { + try { + mAvatar = ImageIO.read(mFile); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't read avatar", ex); + mAvatar = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + } + } + + return mAvatar; + } + + public void set(BufferedImage avatar) { + try { + ImageIO.write(avatar, EXT, mFile); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't save avatar", ex); + } + + mAvatar = avatar; + } +} From e502e57976e7894b3f9b923815740391701ddd04 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 19:11:32 +0100 Subject: [PATCH 086/257] control: added user avatar --- src/main/java/org/kontalk/system/Control.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index ddfaba8f..d1eb09ae 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -18,7 +18,11 @@ package org.kontalk.system; +import org.kontalk.model.UserAvatar; import org.kontalk.model.Account; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -30,6 +34,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import javax.imageio.ImageIO; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.Kontalk; @@ -86,6 +91,7 @@ public enum Status { private final RosterHandler mRosterHandler; private final AvatarHandler mAvatarHandler; private final GroupControl mGroupControl; + private final UserAvatar mAvatar; private Status mCurrentStatus = Status.DISCONNECTED; @@ -98,6 +104,7 @@ private Control() { mRosterHandler = new RosterHandler(this, mClient); mAvatarHandler = new AvatarHandler(mClient); mGroupControl = new GroupControl(this); + mAvatar = new UserAvatar(appDir); } public RosterHandler getRosterHandler() { @@ -750,6 +757,23 @@ public void sendAttachment(Chat chat, Path file){ this.sendTextMessage(chat, "", file); } + public BufferedImage getUserAvatar() { + return mAvatar.get(); + } + + public void setUserAvatar(BufferedImage avatar) { + mAvatar.set(avatar); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + ImageIO.write(avatar, UserAvatar.EXT, out); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't convert avatar", ex); + return; + } + byte[] avatarData = out.toByteArray(); + mClient.publishAvatar(DigestUtils.sha1Hex(avatarData), avatarData); + } + /* private */ private void sendTextMessage(Chat chat, String text, Path file) { From efcf5c96ee2e0110478eb2ddc6ed0ec2d4c27a28 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 10 Dec 2015 19:12:44 +0100 Subject: [PATCH 087/257] view: user avatar in status dialog --- .../java/org/kontalk/view/ComponentUtils.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 02d840ba..dcd7f96b 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -25,6 +25,7 @@ import com.alee.laf.button.WebButton; import com.alee.laf.button.WebToggleButton; import com.alee.laf.checkbox.WebCheckBox; +import com.alee.laf.filechooser.WebFileChooser; import com.alee.laf.label.WebLabel; import com.alee.laf.list.WebList; import com.alee.laf.panel.WebPanel; @@ -37,7 +38,9 @@ import com.alee.managers.popup.PopupAdapter; import com.alee.managers.popup.WebPopup; import com.alee.managers.tooltip.TooltipManager; +import com.alee.utils.ImageUtils; import com.alee.utils.SwingUtils; +import com.alee.utils.filefilter.ImageFilesFilter; import com.alee.utils.swing.DocumentChangeListener; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.awt.BorderLayout; @@ -57,6 +60,8 @@ import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowStateListener; +import java.awt.image.BufferedImage; +import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -90,7 +95,9 @@ import org.kontalk.misc.JID; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; +import org.kontalk.model.UserAvatar; import org.kontalk.system.Config; +import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; @@ -124,9 +131,13 @@ static class ScrollPane extends WebScrollPane { static class StatusDialog extends WebDialog { private final View mView; + private final WebFileChooser mImgChooser; + private final WebImage mAvatarImage; private final WebTextField mStatusField; private final WebList mStatusList; + private BufferedImage mAvatar = null; + StatusDialog(View view) { mView = view; @@ -137,6 +148,32 @@ static class StatusDialog extends WebDialog { GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); groupPanel.setMargin(View.MARGIN_BIG); + groupPanel.add(new WebLabel(Tr.tr("Setup your profile")).setBoldFont()); + groupPanel.add(new WebSeparator(true, true)); + + mImgChooser = new WebFileChooser(); + mImgChooser.setFileFilter(new ImageFilesFilter()); + + groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); + BufferedImage avatar = mView.getControl().getUserAvatar(); + if (avatar.getWidth() == 1) + avatar = AvatarLoader.createFallback(UserAvatar.SIZE); + mAvatarImage = new WebImage(avatar); + + //mAvatarImage.setDisplayType(DisplayType.fitComponent); + //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); + mAvatarImage.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (!e.isPopupTrigger()) { + StatusDialog.this.chooseAvatar(); + } + } + + }); + groupPanel.add(mAvatarImage); + groupPanel.add(new WebSeparator(true, true)); + String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); List stats = new ArrayList<>(Arrays.asList(strings)); String currentStatus = ""; @@ -190,7 +227,29 @@ public void actionPerformed(ActionEvent e) { this.pack(); } + private void chooseAvatar() { + int state = mImgChooser.showOpenDialog(this); + if (state != WebFileChooser.APPROVE_OPTION) + return; + + File imgFile = mImgChooser.getSelectedFile(); + if (!imgFile.isFile()) + return; + + BufferedImage img = MediaUtils.readImage(imgFile).orElse(null); + if (img == null) + return; + + mAvatar = ImageUtils.createPreviewImage(img, UserAvatar.SIZE); + + mAvatarImage.setImage(mAvatar); + } + private void saveStatus() { + if (mAvatar != null) { + mView.getControl().setUserAvatar(mAvatar); + } + mView.getControl().setStatusText(mStatusField.getText()); } } From 9065c41a5d7f2dfacc9a6b5434e1ab9632978efa Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Jan 2016 19:56:14 +0100 Subject: [PATCH 088/257] model: moved contact and user avatar to own class file --- src/main/java/org/kontalk/model/Avatar.java | 191 ++++++++++++++++++ src/main/java/org/kontalk/model/Contact.java | 77 +------ .../java/org/kontalk/model/UserAvatar.java | 76 ------- .../org/kontalk/system/AvatarHandler.java | 6 +- src/main/java/org/kontalk/system/Control.java | 27 +-- .../java/org/kontalk/view/AvatarLoader.java | 2 +- .../java/org/kontalk/view/ComponentUtils.java | 11 +- 7 files changed, 211 insertions(+), 179 deletions(-) create mode 100644 src/main/java/org/kontalk/model/Avatar.java delete mode 100644 src/main/java/org/kontalk/model/UserAvatar.java diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java new file mode 100644 index 00000000..eba219f3 --- /dev/null +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -0,0 +1,191 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.model; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.imageio.ImageIO; +import org.apache.commons.codec.digest.DigestUtils; +import org.kontalk.Kontalk; +import org.kontalk.util.MediaUtils; + + +/** + * Avatar image. Immutable. + * + * @author Alexander Bikadorov {@literal } + */ +public class Avatar { + private static final Logger LOGGER = Logger.getLogger(Avatar.class.getName()); + + private static final String DIR = "avatars"; + protected static final String FORMAT = "png"; + + /** SHA1 hash of image data. */ + private final String mID; + private final File mFile; + + protected BufferedImage mImage = null; + + /** Saved contact avatar. Used when loading from database. */ + Avatar(String id) { + mID = id; + mFile = Kontalk.appDir().resolve(DIR) + .resolve(mID + "." + FORMAT).toFile(); + } + + /** New contact avatar. */ + public Avatar(String id, BufferedImage image) { + this(id); + this.mImage = image; + + save(id, image, mFile); + } + + private Avatar(File file) { + mFile = file; + mImage = image(mFile); + mID = mImage != null ? id(mImage) : ""; + } + + private Avatar(String id, File file, BufferedImage image) { + mID = id; + mFile = file; + mImage = image; + + save(mID, image, mFile); + } + + private static BufferedImage image(File file) { + return MediaUtils.readImage(file).orElse(null); + } + + protected static void save(String id, BufferedImage image, File file) { + boolean succ = MediaUtils.writeImage(image, FORMAT, file); + if (!succ) + LOGGER.warning("can't save avatar image: "+id); + } + + + public String getID() { + return mID; + } + + public Optional loadImage() { + if (mImage == null) + mImage = image(this.mFile); + + return Optional.ofNullable(mImage); + } + + void delete() { + boolean succ = this.mFile.delete(); + if (succ) + LOGGER.warning("could not delete avatar file: "+this.mID); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof Avatar)) return false; + + Avatar oAvatar = (Avatar) o; + return mID.equals(oAvatar.mID); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 59 * hash + Objects.hashCode(this.mID); + return hash; + } + + public static class UserAvatar extends Avatar { + + private static final String USER_FILENAME = "avatar"; + + private static UserAvatar USER_AVATAR = null; + + private byte[] mImageData = null; + + /** Saved user Avatar. */ + UserAvatar() { + super(userFile()); + } + + /** New user Avatar. ID generated from image. */ + UserAvatar(BufferedImage image) { + super(id(image), userFile(), image); + } + + public Optional imageData() { + if (mImageData == null) + mImageData = Avatar.imageData(mImage); + + return Optional.ofNullable(mImageData); + } + + private static File userFile() { + return Kontalk.appDir().resolve(USER_FILENAME + "." + FORMAT).toFile(); + } + + public static UserAvatar instance() { + if (USER_AVATAR == null) { + USER_AVATAR = new UserAvatar(); + } + return USER_AVATAR; + } + + public static UserAvatar setImage(BufferedImage img) { + // TODO reduce size ? + USER_AVATAR = new UserAvatar(img); + return USER_AVATAR; + } + } + + public static void createDir() { + boolean created = Kontalk.appDir().resolve(DIR).toFile().mkdir(); + if (created) + LOGGER.info("created avatar directory"); + } + + private static String id(BufferedImage image) { + byte[] imageData = imageData(image); + return imageData != null ? DigestUtils.sha1Hex(imageData) : ""; + } + + private static byte[] imageData(BufferedImage image) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + ImageIO.write(image, FORMAT, out); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't convert avatar", ex); + return null; + } + return out.toByteArray(); + } +} + diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 9b04044b..822cb8f8 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -18,8 +18,6 @@ package org.kontalk.model; -import java.awt.image.BufferedImage; -import java.io.File; import java.sql.ResultSet; import java.sql.SQLException; import org.kontalk.misc.JID; @@ -28,16 +26,13 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Presence; -import org.kontalk.Kontalk; import org.kontalk.system.Database; import org.kontalk.util.EncodingUtils; -import org.kontalk.util.MediaUtils; import org.kontalk.util.XMPPUtils; /** @@ -324,7 +319,7 @@ private void save() { set.put(COL_ENCR, mEncrypted); set.put(COL_PUB_KEY, Database.setString(mKey)); set.put(COL_KEY_FP, Database.setString(mFingerprint)); - set.put(COL_AVATAR_ID, Database.setString(mAvatar != null ? mAvatar.id : "")); + set.put(COL_AVATAR_ID, Database.setString(mAvatar != null ? mAvatar.getID() : "")); db.execUpdate(TABLE, set, mID); } @@ -354,74 +349,4 @@ static Contact load(ResultSet rs) throws SQLException { return new Contact(id, jid, name, status, Optional.ofNullable(lastSeen), encr, key, fp, avatarID); } - - /** - * Contact avatar image. Immutable. - */ - public static class Avatar { - - private static final String AVATAR_FORMAT = "png"; - private static final String AVATAR_DIR = "avatars"; - - /** SHA1 hash of image data. */ - public final String id; - - private BufferedImage image = null; - - public Avatar(String id, BufferedImage image) { - this.id = id; - this.image = image; - - // save - boolean succ = MediaUtils.writeImage(this.image, AVATAR_FORMAT, this.file()); - if (!succ) - LOGGER.warning("can't save avatar image: "+this.id); - } - - // used when loading from database - Avatar(String id) { - this.id = id; - } - - public Optional loadImage() { - if (image == null) - image = MediaUtils.readImage(this.file()).orElse(null); - - return Optional.ofNullable(image); - } - - void delete() { - boolean succ = this.file().delete(); - if (succ) - LOGGER.warning("could not delete avatar file: "+this.id); - } - - private File file() { - return Kontalk.appDir().resolve(AVATAR_DIR) - .resolve(this.id + "." + AVATAR_FORMAT).toFile(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (!(o instanceof Avatar)) return false; - - Avatar oAvatar = (Avatar) o; - return id.equals(oAvatar.id); - } - - @Override - public int hashCode() { - int hash = 7; - hash = 59 * hash + Objects.hashCode(this.id); - return hash; - } - - public static void createDir() { - boolean created = Kontalk.appDir().resolve(AVATAR_DIR).toFile().mkdir(); - if (created) - LOGGER.info("created avatar directory"); - } - } } diff --git a/src/main/java/org/kontalk/model/UserAvatar.java b/src/main/java/org/kontalk/model/UserAvatar.java deleted file mode 100644 index 5a091b83..00000000 --- a/src/main/java/org/kontalk/model/UserAvatar.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kontalk.model; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.imageio.ImageIO; - -/** - * TODO new class here or include in control? - * @author Alexander Bikadorov {@literal } - */ -public final class UserAvatar { - private static final Logger LOGGER = Logger.getLogger(UserAvatar.class.getName()); - - public static final int SIZE = 150; - - private static final String FILENAME = "avatar"; - public static final String EXT = "jpg"; - - private final File mFile; - - private BufferedImage mAvatar = null; - - public UserAvatar(Path appDir) { - mFile = appDir.resolve(FILENAME + "." + EXT).toFile(); - } - - public BufferedImage get() { - if (mAvatar != null) - return mAvatar; - - if (!mFile.exists()) { - mAvatar = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); - } else { - try { - mAvatar = ImageIO.read(mFile); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't read avatar", ex); - mAvatar = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); - } - } - - return mAvatar; - } - - public void set(BufferedImage avatar) { - try { - ImageIO.write(avatar, EXT, mFile); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't save avatar", ex); - } - - mAvatar = avatar; - } -} diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index b083785d..c1a5bf08 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -26,8 +26,8 @@ import org.apache.commons.codec.digest.DigestUtils; import org.kontalk.client.Client; import org.kontalk.misc.JID; +import org.kontalk.model.Avatar; import org.kontalk.model.Contact; -import org.kontalk.model.Contact.Avatar; import org.kontalk.model.ContactList; import org.kontalk.util.MediaUtils; @@ -47,7 +47,7 @@ public final class AvatarHandler { AvatarHandler(Client client) { mClient = client; - Contact.Avatar.createDir(); + Avatar.createDir(); } public void onNotify(JID jid, String id) { @@ -63,7 +63,7 @@ public void onNotify(JID jid, String id) { } Avatar avatar = contact.getAvatar().orElse(null); - if (avatar != null && avatar.id.equals(id)) + if (avatar != null && avatar.getID().equals(id)) // avatar is not new return; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index d1eb09ae..5a769cc9 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -18,11 +18,8 @@ package org.kontalk.system; -import org.kontalk.model.UserAvatar; import org.kontalk.model.Account; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -34,7 +31,6 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; -import javax.imageio.ImageIO; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.Kontalk; @@ -54,6 +50,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.misc.JID; +import org.kontalk.model.Avatar; import org.kontalk.model.GroupChat; import org.kontalk.model.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.Attachment; @@ -91,7 +88,6 @@ public enum Status { private final RosterHandler mRosterHandler; private final AvatarHandler mAvatarHandler; private final GroupControl mGroupControl; - private final UserAvatar mAvatar; private Status mCurrentStatus = Status.DISCONNECTED; @@ -104,7 +100,6 @@ private Control() { mRosterHandler = new RosterHandler(this, mClient); mAvatarHandler = new AvatarHandler(mClient); mGroupControl = new GroupControl(this); - mAvatar = new UserAvatar(appDir); } public RosterHandler getRosterHandler() { @@ -757,21 +752,17 @@ public void sendAttachment(Chat chat, Path file){ this.sendTextMessage(chat, "", file); } - public BufferedImage getUserAvatar() { - return mAvatar.get(); + public Optional getUserAvatar() { + return Avatar.UserAvatar.instance().loadImage(); } - public void setUserAvatar(BufferedImage avatar) { - mAvatar.set(avatar); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - ImageIO.write(avatar, UserAvatar.EXT, out); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't convert avatar", ex); + public void setUserAvatar(BufferedImage image) { + Avatar.UserAvatar avatar = Avatar.UserAvatar.setImage(image); + byte[] avatarData = avatar.imageData().orElse(null); + if (avatarData == null) return; - } - byte[] avatarData = out.toByteArray(); - mClient.publishAvatar(DigestUtils.sha1Hex(avatarData), avatarData); + + mClient.publishAvatar(avatar.getID(), avatarData); } /* private */ diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 8fa3426e..e915ecca 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -29,9 +29,9 @@ import java.util.Map; import java.util.Objects; import org.apache.commons.lang.ObjectUtils; +import org.kontalk.model.Avatar; import org.kontalk.model.Chat; import org.kontalk.model.Contact; -import org.kontalk.model.Contact.Avatar; import org.kontalk.util.MediaUtils; /** diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index dcd7f96b..7c2971e1 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -95,7 +95,6 @@ import org.kontalk.misc.JID; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; -import org.kontalk.model.UserAvatar; import org.kontalk.system.Config; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; @@ -130,6 +129,8 @@ static class ScrollPane extends WebScrollPane { static class StatusDialog extends WebDialog { + private static final int AVATAR_SIZE = 150; + private final View mView; private final WebFileChooser mImgChooser; private final WebImage mAvatarImage; @@ -155,9 +156,9 @@ static class StatusDialog extends WebDialog { mImgChooser.setFileFilter(new ImageFilesFilter()); groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); - BufferedImage avatar = mView.getControl().getUserAvatar(); - if (avatar.getWidth() == 1) - avatar = AvatarLoader.createFallback(UserAvatar.SIZE); + BufferedImage avatar = mView.getControl().getUserAvatar().orElse(null); + if (avatar == null) + avatar = AvatarLoader.createFallback(AVATAR_SIZE); mAvatarImage = new WebImage(avatar); //mAvatarImage.setDisplayType(DisplayType.fitComponent); @@ -240,7 +241,7 @@ private void chooseAvatar() { if (img == null) return; - mAvatar = ImageUtils.createPreviewImage(img, UserAvatar.SIZE); + mAvatar = ImageUtils.createPreviewImage(img, AVATAR_SIZE); mAvatarImage.setImage(mAvatar); } From 9369f3e43afd9fad25e46e6c947be4c0997df330 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Jan 2016 20:27:48 +0100 Subject: [PATCH 089/257] view: status dialog moved to new profile dialog class --- .../java/org/kontalk/view/ComponentUtils.java | 136 ------------- src/main/java/org/kontalk/view/MainFrame.java | 2 +- .../java/org/kontalk/view/ProfileDialog.java | 181 ++++++++++++++++++ 3 files changed, 182 insertions(+), 137 deletions(-) create mode 100644 src/main/java/org/kontalk/view/ProfileDialog.java diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 7c2971e1..adab9b3b 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -25,11 +25,9 @@ import com.alee.laf.button.WebButton; import com.alee.laf.button.WebToggleButton; import com.alee.laf.checkbox.WebCheckBox; -import com.alee.laf.filechooser.WebFileChooser; import com.alee.laf.label.WebLabel; import com.alee.laf.list.WebList; import com.alee.laf.panel.WebPanel; -import com.alee.laf.rootpane.WebDialog; import com.alee.laf.scroll.WebScrollPane; import com.alee.laf.separator.WebSeparator; import com.alee.laf.tabbedpane.WebTabbedPane; @@ -38,9 +36,7 @@ import com.alee.managers.popup.PopupAdapter; import com.alee.managers.popup.WebPopup; import com.alee.managers.tooltip.TooltipManager; -import com.alee.utils.ImageUtils; import com.alee.utils.SwingUtils; -import com.alee.utils.filefilter.ImageFilesFilter; import com.alee.utils.swing.DocumentChangeListener; import com.google.i18n.phonenumbers.PhoneNumberUtil; import java.awt.BorderLayout; @@ -60,11 +56,8 @@ import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowStateListener; -import java.awt.image.BufferedImage; -import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedList; @@ -96,7 +89,6 @@ import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.system.Config; -import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; @@ -127,134 +119,6 @@ static class ScrollPane extends WebScrollPane { } } - static class StatusDialog extends WebDialog { - - private static final int AVATAR_SIZE = 150; - - private final View mView; - private final WebFileChooser mImgChooser; - private final WebImage mAvatarImage; - private final WebTextField mStatusField; - private final WebList mStatusList; - - private BufferedImage mAvatar = null; - - StatusDialog(View view) { - mView = view; - - this.setTitle(Tr.tr("Status")); - this.setResizable(false); - this.setModal(true); - - GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); - groupPanel.setMargin(View.MARGIN_BIG); - - groupPanel.add(new WebLabel(Tr.tr("Setup your profile")).setBoldFont()); - groupPanel.add(new WebSeparator(true, true)); - - mImgChooser = new WebFileChooser(); - mImgChooser.setFileFilter(new ImageFilesFilter()); - - groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); - BufferedImage avatar = mView.getControl().getUserAvatar().orElse(null); - if (avatar == null) - avatar = AvatarLoader.createFallback(AVATAR_SIZE); - mAvatarImage = new WebImage(avatar); - - //mAvatarImage.setDisplayType(DisplayType.fitComponent); - //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); - mAvatarImage.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (!e.isPopupTrigger()) { - StatusDialog.this.chooseAvatar(); - } - } - - }); - groupPanel.add(mAvatarImage); - groupPanel.add(new WebSeparator(true, true)); - - String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); - List stats = new ArrayList<>(Arrays.asList(strings)); - String currentStatus = ""; - if (!stats.isEmpty()) - currentStatus = stats.remove(0); - - stats.remove(""); - - groupPanel.add(new WebLabel(Tr.tr("Your current status:"))); - mStatusField = new WebTextField(currentStatus, 30); - groupPanel.add(mStatusField); - groupPanel.add(new WebSeparator(true, true)); - - groupPanel.add(new WebLabel(Tr.tr("Previously used:"))); - mStatusList = new WebList(stats); - mStatusList.setMultiplySelectionAllowed(false); - mStatusList.addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) - return; - mStatusField.setText(mStatusList.getSelectedValue().toString()); - } - }); - WebScrollPane listScrollPane = new ScrollPane(mStatusList); - groupPanel.add(listScrollPane); - this.add(groupPanel, BorderLayout.CENTER); - - // buttons - WebButton cancelButton = new WebButton(Tr.tr("Cancel")); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - StatusDialog.this.dispose(); - } - }); - final WebButton saveButton = new WebButton(Tr.tr("Save")); - saveButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - StatusDialog.this.saveStatus(); - StatusDialog.this.dispose(); - } - }); - this.getRootPane().setDefaultButton(saveButton); - - GroupPanel buttonPanel = new GroupPanel(2, cancelButton, saveButton); - buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); - this.add(buttonPanel, BorderLayout.SOUTH); - - this.pack(); - } - - private void chooseAvatar() { - int state = mImgChooser.showOpenDialog(this); - if (state != WebFileChooser.APPROVE_OPTION) - return; - - File imgFile = mImgChooser.getSelectedFile(); - if (!imgFile.isFile()) - return; - - BufferedImage img = MediaUtils.readImage(imgFile).orElse(null); - if (img == null) - return; - - mAvatar = ImageUtils.createPreviewImage(img, AVATAR_SIZE); - - mAvatarImage.setImage(mAvatar); - } - - private void saveStatus() { - if (mAvatar != null) { - mView.getControl().setUserAvatar(mAvatar); - } - - mView.getControl().setStatusText(mStatusField.getText()); - } - } - static abstract class PopupPanel extends WebPanel { abstract void onShow(); diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 4a3b4047..038d4117 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -140,7 +140,7 @@ public void actionPerformed(ActionEvent event) { statusMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - WebDialog statusDialog = new ComponentUtils.StatusDialog(mView); + WebDialog statusDialog = new ProfileDialog(mView); statusDialog.setVisible(true); } }); diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java new file mode 100644 index 00000000..9a6393ae --- /dev/null +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -0,0 +1,181 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.view; + +import com.alee.extended.image.WebImage; +import com.alee.extended.panel.GroupPanel; +import com.alee.laf.button.WebButton; +import com.alee.laf.filechooser.WebFileChooser; +import com.alee.laf.label.WebLabel; +import com.alee.laf.list.WebList; +import com.alee.laf.rootpane.WebDialog; +import com.alee.laf.scroll.WebScrollPane; +import com.alee.laf.separator.WebSeparator; +import com.alee.laf.text.WebTextField; +import com.alee.utils.ImageUtils; +import com.alee.utils.filefilter.ImageFilesFilter; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import org.kontalk.system.Config; +import org.kontalk.util.MediaUtils; +import org.kontalk.util.Tr; + +/** + * The User profile page. With avatar and status text. + * @author Alexander Bikadorov {@literal } + */ +final class ProfileDialog extends WebDialog { + + private static final int AVATAR_SIZE = 150; + + private final View mView; + private final WebFileChooser mImgChooser; + private final WebImage mAvatarImage; + private final WebTextField mStatusField; + private final WebList mStatusList; + + private BufferedImage mAvatar = null; + + ProfileDialog(View view) { + mView = view; + + this.setTitle(Tr.tr("Status")); + this.setResizable(false); + this.setModal(true); + + GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); + groupPanel.setMargin(View.MARGIN_BIG); + + groupPanel.add(new WebLabel(Tr.tr("Setup your profile")).setBoldFont()); + groupPanel.add(new WebSeparator(true, true)); + + mImgChooser = new WebFileChooser(); + mImgChooser.setFileFilter(new ImageFilesFilter()); + + groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); + BufferedImage avatar = mView.getControl().getUserAvatar().orElse(null); + mAvatarImage = new WebImage(avatar != null ? + avatar : + AvatarLoader.createFallback(AVATAR_SIZE)); + + //mAvatarImage.setDisplayType(DisplayType.fitComponent); + //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); + mAvatarImage.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (!e.isPopupTrigger()) { + ProfileDialog.this.chooseAvatar(); + } + } + + }); + groupPanel.add(mAvatarImage); + groupPanel.add(new WebSeparator(true, true)); + + String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); + List stats = new ArrayList<>(Arrays.asList(strings)); + String currentStatus = ""; + if (!stats.isEmpty()) + currentStatus = stats.remove(0); + + stats.remove(""); + + groupPanel.add(new WebLabel(Tr.tr("Your current status:"))); + mStatusField = new WebTextField(currentStatus, 30); + groupPanel.add(mStatusField); + groupPanel.add(new WebSeparator(true, true)); + + groupPanel.add(new WebLabel(Tr.tr("Previously used:"))); + mStatusList = new WebList(stats); + mStatusList.setMultiplySelectionAllowed(false); + mStatusList.addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) + return; + mStatusField.setText(mStatusList.getSelectedValue().toString()); + } + }); + WebScrollPane listScrollPane = new ComponentUtils.ScrollPane(mStatusList); + groupPanel.add(listScrollPane); + this.add(groupPanel, BorderLayout.CENTER); + + // buttons + WebButton cancelButton = new WebButton(Tr.tr("Cancel")); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ProfileDialog.this.dispose(); + } + }); + final WebButton saveButton = new WebButton(Tr.tr("Save")); + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ProfileDialog.this.saveStatus(); + ProfileDialog.this.dispose(); + } + }); + this.getRootPane().setDefaultButton(saveButton); + + GroupPanel buttonPanel = new GroupPanel(2, cancelButton, saveButton); + buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); + this.add(buttonPanel, BorderLayout.SOUTH); + + this.pack(); + } + + private void chooseAvatar() { + int state = mImgChooser.showOpenDialog(this); + if (state != WebFileChooser.APPROVE_OPTION) + return; + + File imgFile = mImgChooser.getSelectedFile(); + if (!imgFile.isFile()) + return; + + BufferedImage img = MediaUtils.readImage(imgFile).orElse(null); + if (img == null) + return; + + mAvatar = ImageUtils.createPreviewImage(img, AVATAR_SIZE); + + mAvatarImage.setImage(mAvatar); + } + + private void saveStatus() { + if (mAvatar != null) { + mView.getControl().setUserAvatar(mAvatar); + } + + mView.getControl().setStatusText(mStatusField.getText()); + } + +} From 3ce4d745f7ff8d68cae912a66d2c7d7341c0de0c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Jan 2016 20:36:40 +0100 Subject: [PATCH 090/257] client: merged obsolete avatar sender with avatar send-receiver --- .../kontalk/client/AvatarSendReceiver.java | 30 +++++++- .../java/org/kontalk/client/AvatarSender.java | 76 ------------------- src/main/java/org/kontalk/client/Client.java | 10 +-- 3 files changed, 33 insertions(+), 83 deletions(-) delete mode 100644 src/main/java/org/kontalk/client/AvatarSender.java diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index a944d2b1..db5faf32 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -20,17 +20,21 @@ import java.util.Arrays; import java.util.List; +import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smackx.pubsub.Item; import org.jivesoftware.smackx.pubsub.ItemsExtension; +import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PayloadItem; import org.jivesoftware.smackx.pubsub.PubSubElementType; +import org.jivesoftware.smackx.pubsub.PubSubManager; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; import org.kontalk.misc.JID; @@ -73,8 +77,32 @@ final class AvatarSendReceiver { mHandler = handler; } - // TODO beta.kontalk.net does not support this + // TODO beta.kontalk.net does not support this, untested void publish(String id, byte[] data) { + PubSubManager mPubSubManager = new PubSubManager(mConn, mConn.getServiceName()); + LeafNode node; + try { + node = mPubSubManager.createNode(DATA_NODE); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't create node", ex); + return; + } + + PayloadItem item = new PayloadItem<>(id, + new AvatarDataExtension(data)); + try { + // blocking + node.send(item); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't send item", ex); + return; + } + + // publish meta data... } void processMetadataEvent(JID jid, ItemsExtension itemsExt) { diff --git a/src/main/java/org/kontalk/client/AvatarSender.java b/src/main/java/org/kontalk/client/AvatarSender.java deleted file mode 100644 index b06ed4d1..00000000 --- a/src/main/java/org/kontalk/client/AvatarSender.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kontalk.client; - -import java.util.logging.Level; -import java.util.logging.Logger; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smackx.pubsub.LeafNode; -import org.jivesoftware.smackx.pubsub.PayloadItem; -import org.jivesoftware.smackx.pubsub.PubSubManager; - - -/** - * - * Note: Smacks PEPManager is not in a usable state. - * @author Alexander Bikadorov {@literal } - */ -final class AvatarSender { - private static final Logger LOGGER = Logger.getLogger(AvatarSender.class.getName()); - - private static final String DATA_NODE = "urn:xmpp:avatar:data"; - - private final PubSubManager mPubSubManager; - - AvatarSender(PubSubManager m) { - this.mPubSubManager = m; - } - - // TODO beta.kontalk.net does not support this, untested - void publish(String id, byte[] data) { - - LeafNode node; - try { - node = mPubSubManager.createNode(DATA_NODE); - } catch (SmackException.NoResponseException | - XMPPException.XMPPErrorException | - SmackException.NotConnectedException ex) { - LOGGER.log(Level.WARNING, "can't create node", ex); - return; - } - - PayloadItem item = new PayloadItem<>(id, - new AvatarDataExtension(data)); - try { - // blocking - node.send(item); - } catch (SmackException.NoResponseException | - XMPPException.XMPPErrorException | - SmackException.NotConnectedException ex) { - LOGGER.log(Level.WARNING, "can't send item", ex); - return; - } - - // publish meta data... - } - -} - - diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index c97bc814..9069ad0c 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -45,7 +45,6 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; -import org.jivesoftware.smackx.pubsub.PubSubManager; import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; @@ -438,12 +437,11 @@ public void requestAvatar(JID jid, String id) { } public void publishAvatar(String id, byte[] data) { - if (mConn == null) + if (mAvatarSendReceiver == null) { + LOGGER.warning("no avatar sender"); return; - - // TODO - PubSubManager pubSubManager = new PubSubManager(mConn, mConn.getServiceName()); - new AvatarSender(pubSubManager).publish(id, data); + } + mAvatarSendReceiver.publish(id, data); } @Override From c2111ecde84731ed30035cbfc745ad562e3ea80d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 18:17:52 +0100 Subject: [PATCH 091/257] model: fixed warning for unset user avatar image --- src/main/java/org/kontalk/model/Avatar.java | 48 +++++++++++---------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index eba219f3..2566b20c 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -45,50 +45,45 @@ public class Avatar { /** SHA1 hash of image data. */ private final String mID; - private final File mFile; + protected final File mFile; protected BufferedImage mImage = null; /** Saved contact avatar. Used when loading from database. */ Avatar(String id) { - mID = id; - mFile = Kontalk.appDir().resolve(DIR) - .resolve(mID + "." + FORMAT).toFile(); + this(id, null, null); } /** New contact avatar. */ public Avatar(String id, BufferedImage image) { - this(id); - this.mImage = image; - - save(id, image, mFile); - } - - private Avatar(File file) { - mFile = file; - mImage = image(mFile); - mID = mImage != null ? id(mImage) : ""; + this(id, null, image); } private Avatar(String id, File file, BufferedImage image) { mID = id; - mFile = file; + mFile = file != null ? + file : + Kontalk.appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); mImage = image; - save(mID, image, mFile); + if (mImage != null) { + // save new image + boolean succ = MediaUtils.writeImage(image, FORMAT, file); + if (!succ) + LOGGER.warning("can't save avatar image: "+id); + } } - private static BufferedImage image(File file) { - return MediaUtils.readImage(file).orElse(null); + private Avatar(File file) { + mFile = file; + mImage = file.isFile() ? image(mFile) : null; + mID = mImage != null ? id(mImage) : ""; } - protected static void save(String id, BufferedImage image, File file) { - boolean succ = MediaUtils.writeImage(image, FORMAT, file); - if (!succ) - LOGGER.warning("can't save avatar image: "+id); + private static BufferedImage image(File file) { + return MediaUtils.readImage(file).orElse(null); } - public String getID() { return mID; } @@ -141,6 +136,13 @@ public static class UserAvatar extends Avatar { super(id(image), userFile(), image); } + @Override + public Optional loadImage() { + return mFile.isFile() ? + Optional.ofNullable(image(mFile)) : + Optional.empty(); + } + public Optional imageData() { if (mImageData == null) mImageData = Avatar.imageData(mImage); From ca7c2173bef3afccc77d20573cc7dbbfbf7c325e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 19:51:06 +0100 Subject: [PATCH 092/257] model: scale user avatar image down to max size --- src/main/java/org/kontalk/model/Avatar.java | 8 ++- .../java/org/kontalk/util/MediaUtils.java | 59 +++++++++++++++---- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 2566b20c..37a3a2f7 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -120,6 +120,7 @@ public int hashCode() { public static class UserAvatar extends Avatar { + private static final int MAX_SIZE = 150; private static final String USER_FILENAME = "avatar"; private static UserAvatar USER_AVATAR = null; @@ -161,9 +162,9 @@ public static UserAvatar instance() { return USER_AVATAR; } - public static UserAvatar setImage(BufferedImage img) { - // TODO reduce size ? - USER_AVATAR = new UserAvatar(img); + public static UserAvatar setImage(BufferedImage image) { + image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE, true); + USER_AVATAR = new UserAvatar(image); return USER_AVATAR; } } @@ -188,6 +189,7 @@ private static byte[] imageData(BufferedImage image) { return null; } return out.toByteArray(); + } } diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 4edccf7b..690e2b6f 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -28,6 +28,8 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; @@ -35,7 +37,6 @@ import org.apache.tika.mime.MimeType; import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypes; -import org.kontalk.misc.Callback; /** * @@ -145,13 +146,13 @@ public static byte[] imageToByteArray(Image image, String format) { } /** - * Scale image down to maximum or minimum of width or height, preserving ratio. - * @param max specifies if image is scaled to maximum or minimum of width/height - * @return the scaled image, loaded async + * Scale image down preserving ratio. + * Blocking. */ - public static void scale(Image image, int width, int height, boolean max, - final Callback.Handler handler) { - Image img = scaleAsync(image, width, height, max); + public static BufferedImage scale(Image image, int width, int height, boolean max) { + image = scaleAsync(image, width, height, max); + + final CountDownLatch latch = new CountDownLatch(1); ImageObserver observer = new ImageObserver() { @Override @@ -161,17 +162,53 @@ public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, in return true; } - handler.handle(new Callback<>(img)); + // scaling done, continue with calling thread + latch.countDown(); return false; } }; - if (img.getWidth(observer) == -1) - return; + if (image.getWidth(observer) == -1) { + boolean succ = false; + try { + succ = latch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + LOGGER.log(Level.WARNING, "interrupted", ex); + } + if (!succ) { + LOGGER.warning("await failed"); + } + } + + return toBufferedImage(image); + } + + // source: https://stackoverflow.com/a/13605411 + private static BufferedImage toBufferedImage(Image image) { + if (image instanceof BufferedImage) + return (BufferedImage) image; + + int iw = image.getWidth(null); + int ih = image.getHeight(null); + if (iw == -1) { + LOGGER.warning("image not loaded yet"); + } + + BufferedImage bimage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB); + + Graphics2D bGr = bimage.createGraphics(); + bGr.drawImage(image, 0, 0, null); + bGr.dispose(); - handler.handle(new Callback<>(img)); + return bimage; } + /** + * Scale image down to maximum or minimum of width or height, preserving ratio. + * Async: returned image may not fully loaded. + * + * @param max specifies if image is scaled to maximum or minimum of width/height + */ public static Image scaleAsync(Image image, int width, int height, boolean max) { int iw = image.getWidth(null); int ih = image.getHeight(null); From 541156df90bb83e0285bee3e3782302b97974363 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 19:52:49 +0100 Subject: [PATCH 093/257] control/client: minor fixes for user avatar --- src/main/java/org/kontalk/client/AvatarSendReceiver.java | 5 +++++ src/main/java/org/kontalk/system/Control.java | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index db5faf32..6f89b69d 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -79,6 +79,11 @@ final class AvatarSendReceiver { // TODO beta.kontalk.net does not support this, untested void publish(String id, byte[] data) { + if (!mConn.isAuthenticated()) { + LOGGER.info("not logged in"); + return; + } + PubSubManager mPubSubManager = new PubSubManager(mConn, mConn.getServiceName()); LeafNode node; try { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 5a769cc9..be50ee15 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -132,6 +132,8 @@ public void setStatus(Status status) { for (Contact contact : ContactList.getInstance()) if (contact.getFingerprint().isEmpty()) this.maySendKeyRequest(contact); + + // TODO check current user avatar on server and upload if necessary } else if (status == Status.DISCONNECTED || status == Status.FAILED) { for (Contact contact : ContactList.getInstance()) contact.setOffline(); @@ -759,7 +761,7 @@ public Optional getUserAvatar() { public void setUserAvatar(BufferedImage image) { Avatar.UserAvatar avatar = Avatar.UserAvatar.setImage(image); byte[] avatarData = avatar.imageData().orElse(null); - if (avatarData == null) + if (avatarData == null || avatar.getID().isEmpty()) return; mClient.publishAvatar(avatar.getID(), avatarData); From b1a35f726607bab5b85c18e52f029a8a7980187b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 19:53:38 +0100 Subject: [PATCH 094/257] view: renamed ui labels: "status" -> "profile" --- src/main/java/org/kontalk/view/MainFrame.java | 4 ++-- src/main/java/org/kontalk/view/ProfileDialog.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 038d4117..00dffba1 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -134,9 +134,9 @@ public void actionPerformed(ActionEvent event) { konNetMenu.add(mDisconnectMenuItem); konNetMenu.addSeparator(); - WebMenuItem statusMenuItem = new WebMenuItem(Tr.tr("Set status")); + WebMenuItem statusMenuItem = new WebMenuItem(Tr.tr("Profile")); statusMenuItem.setAccelerator(Hotkey.ALT_S); - statusMenuItem.setToolTipText(Tr.tr("Set status text send to other user")); + statusMenuItem.setToolTipText(Tr.tr("Set your user profile")); statusMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 9a6393ae..4c7bf535 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -66,14 +66,14 @@ final class ProfileDialog extends WebDialog { ProfileDialog(View view) { mView = view; - this.setTitle(Tr.tr("Status")); + this.setTitle(Tr.tr("User Profile")); this.setResizable(false); this.setModal(true); GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); groupPanel.setMargin(View.MARGIN_BIG); - groupPanel.add(new WebLabel(Tr.tr("Setup your profile")).setBoldFont()); + groupPanel.add(new WebLabel(Tr.tr("Edit your profile")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); mImgChooser = new WebFileChooser(); @@ -109,6 +109,7 @@ public void mouseClicked(MouseEvent e) { groupPanel.add(new WebLabel(Tr.tr("Your current status:"))); mStatusField = new WebTextField(currentStatus, 30); + mStatusField.setToolTipText(Tr.tr("Set status text send to other user")); groupPanel.add(mStatusField); groupPanel.add(new WebSeparator(true, true)); From 3591d06a2a770a4fe16fc027f60219dcd4113ce4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 20:05:00 +0100 Subject: [PATCH 095/257] test: added test for crypto utils --- .../java/org/kontalk/util/CryptoUtils.java | 6 +- .../org/kontalk/util/CryptoUtilsTest.java | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/kontalk/util/CryptoUtilsTest.java diff --git a/src/main/java/org/kontalk/util/CryptoUtils.java b/src/main/java/org/kontalk/util/CryptoUtils.java index 1d379dab..e869c0e0 100644 --- a/src/main/java/org/kontalk/util/CryptoUtils.java +++ b/src/main/java/org/kontalk/util/CryptoUtils.java @@ -38,11 +38,11 @@ public class CryptoUtils { * Ugly hack to get “unlimited strength” for the Java Encryption Extension. * Source: https://stackoverflow.com/a/22492582 */ - public static void removeCryptographyRestrictions() { + public static boolean removeCryptographyRestrictions() { try { if (Cipher.getMaxAllowedKeyLength("RC5") >= 256) { LOGGER.config("cryptography restrictions removal not needed"); - return; + return true; } } catch (NoSuchAlgorithmException ex) { LOGGER.log(Level.WARNING, "can't check for crypto restriction", ex); } @@ -82,6 +82,8 @@ public static void removeCryptographyRestrictions() { IllegalArgumentException | IllegalAccessException ex) { LOGGER.log(Level.WARNING, "can't remove cryptography restrictions", ex); + return false; } + return true; } } diff --git a/src/test/java/org/kontalk/util/CryptoUtilsTest.java b/src/test/java/org/kontalk/util/CryptoUtilsTest.java new file mode 100644 index 00000000..664fc1b3 --- /dev/null +++ b/src/test/java/org/kontalk/util/CryptoUtilsTest.java @@ -0,0 +1,64 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.kontalk.util; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +public class CryptoUtilsTest { + + public CryptoUtilsTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of removeCryptographyRestrictions method, of class CryptoUtils. + * NOTE: crypto restriction removal is platform dependend, so is the + * result of this test + */ + @Test + public void testRemoveCryptographyRestrictions() { + System.out.println("removeCryptographyRestrictions"); + boolean succ = CryptoUtils.removeCryptographyRestrictions(); + assert (succ); + // TODO better test by trying out + } + +} From 0d48cb2138f97f26d69a3f172d8a6b9e4d4d0c76 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 20:50:24 +0100 Subject: [PATCH 096/257] updated README: more advertisement --- README.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a9470788..60ca9a59 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,24 @@ The desktop client uses your existing Kontalk account from the [Android client]( ## Key Features -- connecting to Kontalk server with an already existing Kontalk account -- automatically adding XMPP roster entries from server -- manually adding arbitrary Kontalk or Jabber user -- automatically requesting/adding public keys for other Kontalk user -- sending/receiving (encrypted) text messages from/to Kontalk user -- sending/receiving (plain) text messages from/to arbitrary Jabber/XMPP user (clients like [Pidgin](https://pidgin.im/) or [Conversations](https://github.com/siacs/Conversations)) -- sending/requesting server receipts according to XMPP extension -- ability to block all messages for specific user -- receiving files send from the Android client +Connect with Kontalk... +- Use the existing Kontalk account from your phone. +- Synchronized contact list (=XMPP roster). +- Add new Kontalk users by phone number. +- The client automatically requests public keys for save communication. +- Your communication with Kontalk users is encrypted by default. + +... and beyond: +- Exchange text messages with any Jabber/XMPP users! +- Add new Jabber contacts by JID. +- Tested with clients like [Pidgin](https://pidgin.im/) or [Conversations](https://github.com/siacs/Conversations). + +## Implemented XEP features: +- XEP-0184: Message receipts +- XEP-0085: Chat state notifications +- XEP-0191: User blocking +- XEP-0066: File transfer over server +- XEP-0084: Avatar images **Note: private key and messages are saved unencrypted and can be read by other applications on your computer!** From 0c504c4e8d5be80f5863d4f4a33885be3f835711 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Jan 2016 20:51:41 +0100 Subject: [PATCH 097/257] (auto)update translation strings --- src/main/resources/i18n/strings.properties | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 6eccb8c1..c386da90 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -37,7 +37,6 @@ s_VTOO = Connect s_J8B9 = Connect to Server s_2939 = Disconnect s_XW05 = Disconnect from Server -s_RYMX = Set status s_66AA = Set status text send to other user s_AN83 = Exit s_NLZ0 = Exit application @@ -237,3 +236,8 @@ s_MNIJ = Waiting... s_0WUC = Not verified s_9S4L = Authorization request s_FDTC = When accepting, this contact will be able to see your online status. +s_TAG1 = User Profile +s_84VP = Edit your profile +s_4QXX = Your profile picture: +s_1R7I = Profile +s_O2UH = Set your user profile From aa9d14d127d1d064a7d2b0a2b53deec0540ed8ea Mon Sep 17 00:00:00 2001 From: Toinou Date: Wed, 27 Jan 2016 21:35:09 +0000 Subject: [PATCH 098/257] Translated using Weblate (French) Currently translated at 100.0% (238 of 238 strings) --- src/main/resources/i18n/strings_fr.properties | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/resources/i18n/strings_fr.properties b/src/main/resources/i18n/strings_fr.properties index 64b62091..11439744 100644 --- a/src/main/resources/i18n/strings_fr.properties +++ b/src/main/resources/i18n/strings_fr.properties @@ -223,3 +223,35 @@ s_XITD=Pas atteignable s_NCU0=Erreur de contact s_NTTQ=Erreur : s_L4ZD=Serveur introuvable +s_KPWG=Vide +s_ZX32=Effacé +s_LBBT=Vous +s_ZUZ9=Échec de téléchargement du fichier +s_JRCQ=Échec d'envoi du fichier +s_JSD2=Erreur inhabituelle : +s_BLFT=Vous allez automatiquement quitter ce groupe. +s_74SC=Sélectionnez les participants : +s_B5NX=Éditer le groupe de discussion +s_U5RT=a crée ce groupe +s_5X07=a quitté ce groupe +s_FWZZ=a changé ce groupe +s_OOS3=Connection… +s_0WD8=Déconnection… +s_Z1GT=Impossible de charger tous les fichiers de clé depuis l'archive. +s_MMBD=Recherche… +s_PC1G=écrit… +s_KRQ6=Entrez le mot de passe… +s_2R2P=chargement… +s_307W=téléchargement… +s_4WC0=Le certificat du serveur n'a pas pu être validé. +s_68LN=Requête +s_7MDX=Demande l'autorisation au contact d'accéder à son statut +s_OTEU=Donner automatiquement l'autorisation +s_MFZY=Donner automatiquement aux autres utilisateurs l'autorisation de voir le statut en ligne +s_FAS6=Envoyer l'activité de conversation (tapotage,...) aux autres utilisateurs +s_29CW=Éditer le Contact +s_AGYA=Éditer les paramètres du contact +s_MNIJ=En attente... +s_0WUC=Non vérifié +s_9S4L=Requête d'autorisation +s_FDTC=En acceptant, ce contact pourra voir votre statut de présence en ligne. From 543f105b739be2a13425a01b7740fa7638342d45 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Jan 2016 22:52:53 +0100 Subject: [PATCH 099/257] Update README.md uh --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 60ca9a59..57d9f56f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Connect with Kontalk... - Use the existing Kontalk account from your phone. - Synchronized contact list (=XMPP roster). - Add new Kontalk users by phone number. -- The client automatically requests public keys for save communication. +- The client automatically requests public keys for safe communication. - Your communication with Kontalk users is encrypted by default. ... and beyond: @@ -33,6 +33,9 @@ Connect with Kontalk... - Add new Jabber contacts by JID. - Tested with clients like [Pidgin](https://pidgin.im/) or [Conversations](https://github.com/siacs/Conversations). +**Note: private key and messages are saved unencrypted and can be read by other +applications on your computer!** + ## Implemented XEP features: - XEP-0184: Message receipts - XEP-0085: Chat state notifications @@ -40,9 +43,6 @@ Connect with Kontalk... - XEP-0066: File transfer over server - XEP-0084: Avatar images -**Note: private key and messages are saved unencrypted and can be read by other -applications on your computer!** - ## Support us * If you are missing a feature or found a bug [report it!](https://github.com/kontalk/desktopclient-java/issues) From 92d2049e6eeb0ca6afae47e56abfd312a9f2b242 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 28 Jan 2016 20:03:57 +0100 Subject: [PATCH 100/257] updated client-common-java; Smack version 1.5 --- client-common-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common-java b/client-common-java index 52f5e57f..7d18a659 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit 52f5e57f6249ba9428cc83023071fe58d879259d +Subproject commit 7d18a6595a47375672102d0fcd4a82e1ca56e1ed From 644997a0c80ccc63743f321d7743148c532f9dcd Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 28 Jan 2016 21:02:58 +0100 Subject: [PATCH 101/257] client: added server feature discovery (unused) --- src/main/java/org/kontalk/client/Client.java | 56 ++++++++++++++----- .../kontalk/client/KonConnectionListener.java | 1 + 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 9069ad0c..e09506de 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -21,6 +21,8 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; @@ -45,6 +47,8 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; +import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; @@ -66,12 +70,15 @@ public final class Client implements StanzaListener, Runnable { private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); public enum PresenceCommand {REQUEST, GRANT, DENY}; + public enum ServerFeature {PUBSUB} - private static enum Command {CONNECT, DISCONNECT}; + private enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; + private final HashMap mFeatureMap; + private final EnumSet mFeatures; private KonConnection mConn = null; private AvatarSendReceiver mAvatarSendReceiver = null; @@ -85,6 +92,10 @@ public Client(Control control) { // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; + mFeatureMap = new HashMap<>(); + mFeatureMap.put(PubSub.NAMESPACE, ServerFeature.PUBSUB); + mFeatures = EnumSet.noneOf(ServerFeature.class); + // setting caps cache File cacheDir = Kontalk.appDir().resolve(CAPS_CACHE_DIR).toFile(); if (cacheDir.mkdir()) @@ -191,19 +202,28 @@ private void connectAsync() { // (server) service discovery, XEP-0030 // NOTE: smack automatically creates instances of SDM and CapsM and connects them - //ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn); -// try { -// // blocking -// // NOTE: null parameter does not work -// DiscoverInfo i = discoManager.discoverInfo(mConn.getServiceName()); -// for (DiscoverInfo.Feature f: i.getFeatures()) { -// System.out.println("server feature: "+f.getVar()); -// } -// } catch (SmackException.NoResponseException | -// XMPPException.XMPPErrorException | -// SmackException.NotConnectedException ex) { -// Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex); -// } + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn); + DiscoverInfo info = null; + try { + // blocking + // NOTE: null parameter does not work + info = discoManager.discoverInfo(mConn.getServiceName()); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't get service discovery info"); + } + + mFeatures.clear(); + if (info != null) { + for (DiscoverInfo.Feature feature: info.getFeatures()) { + String var = feature.getVar(); + if (mFeatureMap.containsKey(var)) { + mFeatures.add(mFeatureMap.get(feature)); + } + } + } + LOGGER.info("supported server features: "+mFeatures); // Caps, XEP-0115 // NOTE: caps manager is automatically used by Smack @@ -258,7 +278,6 @@ public void disconnect() { mConn.disconnect(); } } - mControl.setStatus(Control.Status.DISCONNECTED); } public boolean isConnected() { @@ -444,6 +463,13 @@ public void publishAvatar(String id, byte[] data) { mAvatarSendReceiver.publish(id, data); } + public EnumSet getServerFeatures() { + if (!this.isConnected()) + mFeatures.clear(); + + return mFeatures.clone(); + } + @Override public void run() { while (true) { diff --git a/src/main/java/org/kontalk/client/KonConnectionListener.java b/src/main/java/org/kontalk/client/KonConnectionListener.java index fa02fbbf..f0f7d4b5 100644 --- a/src/main/java/org/kontalk/client/KonConnectionListener.java +++ b/src/main/java/org/kontalk/client/KonConnectionListener.java @@ -58,6 +58,7 @@ public void authenticated(XMPPConnection connection, boolean resumed) { public void connectionClosed() { mConnected = false; LOGGER.info("connection closed"); + mControl.setStatus(Control.Status.DISCONNECTED); } @Override From e391918a2b0679fb660948a85c358f1cef9a7410 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 28 Jan 2016 21:20:00 +0100 Subject: [PATCH 102/257] view: minor rework in profile dialog --- src/main/java/org/kontalk/view/ProfileDialog.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 4c7bf535..f780d866 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -61,8 +61,6 @@ final class ProfileDialog extends WebDialog { private final WebTextField mStatusField; private final WebList mStatusList; - private BufferedImage mAvatar = null; - ProfileDialog(View view) { mView = view; @@ -140,7 +138,7 @@ public void actionPerformed(ActionEvent e) { saveButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - ProfileDialog.this.saveStatus(); + ProfileDialog.this.save(); ProfileDialog.this.dispose(); } }); @@ -166,14 +164,13 @@ private void chooseAvatar() { if (img == null) return; - mAvatar = ImageUtils.createPreviewImage(img, AVATAR_SIZE); - - mAvatarImage.setImage(mAvatar); + mAvatarImage.setImage(ImageUtils.createPreviewImage(img, AVATAR_SIZE)); } - private void saveStatus() { - if (mAvatar != null) { - mView.getControl().setUserAvatar(mAvatar); + private void save() { + BufferedImage avatar = mAvatarImage.getImage(); + if (avatar != null) { + mView.getControl().setUserAvatar(avatar); } mView.getControl().setStatusText(mStatusField.getText()); From 842844bd8960a52319ea6acd6d31fdd81d787f4f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 15:39:02 +0100 Subject: [PATCH 103/257] model: introduced new class combining contact, role (not saved to db) and chatstate And Bloch says arrays are no good, sounds legit --- src/main/java/org/kontalk/model/Chat.java | 83 +++++++---- src/main/java/org/kontalk/model/ChatList.java | 10 +- .../java/org/kontalk/model/GroupChat.java | 134 ++++++++++-------- .../java/org/kontalk/model/SingleChat.java | 48 +++---- src/main/java/org/kontalk/system/Control.java | 2 +- .../java/org/kontalk/view/ChatListView.java | 9 +- .../java/org/kontalk/view/MessageList.java | 2 +- src/main/java/org/kontalk/view/Utils.java | 2 +- 8 files changed, 167 insertions(+), 123 deletions(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 572a7da4..f2cba3cb 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -21,9 +21,10 @@ import java.awt.Color; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -31,7 +32,6 @@ import java.util.Observable; import java.util.Observer; import java.util.Optional; -import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; @@ -83,6 +83,9 @@ public abstract class Chat extends Observable implements Observer { "FOREIGN KEY ("+COL_REC_CONTACT_ID+") REFERENCES "+Contact.TABLE+" (_id) " + ")"; + /** Long-live authorization model of member in group. 'Affiliation' in MUC */ + public enum Role {DEFAULT, OWNER, ADMIN}; + protected final int mID; private final ChatMessages mMessages; @@ -91,10 +94,10 @@ public abstract class Chat extends Observable implements Observer { private ViewSettings mViewSettings; protected Chat(Contact contact, String xmppID, String subject) { - this(new Contact[]{contact}, xmppID, subject, null); + this(Arrays.asList(new Member(contact)), xmppID, subject, null); } - protected Chat(Contact[] contacts, String xmppID, String subject, GroupMetaData gData) { + protected Chat(List members, String xmppID, String subject, GroupMetaData gData) { mMessages = new ChatMessages(this, true); mRead = true; mViewSettings = new ViewSettings(); @@ -113,8 +116,8 @@ protected Chat(Contact[] contacts, String xmppID, String subject, GroupMetaData return; } - for (Contact contact : contacts) - this.insertReceiver(contact); + for (Member member : members) + this.insertMember(member); } // used when loading from database @@ -179,7 +182,7 @@ public boolean isGroupChat() { } /** Get all contacts (including deleted, blocked and user contact). */ - public abstract Set getAllContacts(); + public abstract List getAllContacts(); /** Get valid receiver contacts (without deleted and blocked). */ public abstract Contact[] getValidContacts(); @@ -211,7 +214,7 @@ public boolean isGroupChat() { abstract void save(); - protected void save(Contact[] contacts, String subject) { + protected void save(List members, String subject) { Database db = Database.getInstance(); Map set = new HashMap<>(); set.put(COL_SUBJ, Database.setString(subject)); @@ -224,11 +227,11 @@ protected void save(Contact[] contacts, String subject) { Map dbReceiver = loadReceiver(mID); // add missing contact - for (Contact contact : contacts) { - if (!dbReceiver.keySet().contains(contact.getID())) { - this.insertReceiver(contact); + for (Member member : members) { + if (!dbReceiver.keySet().contains(member.contact.getID())) { + this.insertMember(member); } - dbReceiver.remove(contact.getID()); + dbReceiver.remove(member.contact.getID()); } // whats left is too much and can be removed @@ -261,11 +264,11 @@ void delete() { db.execDelete(TABLE, mID); } - private void insertReceiver(Contact contact) { + private void insertMember(Member member) { Database db = Database.getInstance(); List recValues = new LinkedList<>(); recValues.add(mID); - recValues.add(contact.getID()); + recValues.add(member.contact.getID()); int id = db.execInsert(RECEIVER_TABLE, recValues); if (id < 1) { LOGGER.warning("could not insert receiver"); @@ -292,15 +295,15 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String xmppID = Database.getString(rs, Chat.COL_XMPPID); - // get contacts for chats + // get members for chats Map dbReceiver = Chat.loadReceiver(id); - Set contacts = new HashSet<>(); + List members = new ArrayList<>(); for (int conID: dbReceiver.keySet()) { Contact c = ContactList.getInstance().get(conID).orElse(null); if (c != null) - contacts.add(c); + members.add(new Member(c)); else - LOGGER.warning("can't find contact"); + LOGGER.warning("can't find contact, ID:"+conID); } String subject = Database.getString(rs, Chat.COL_SUBJ); @@ -311,13 +314,13 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { Chat.COL_VIEW_SET); if (gData != null) { - return GroupChat.create(id, contacts, gData, subject, read, jsonViewSettings); + return GroupChat.create(id, members, gData, subject, read, jsonViewSettings); } else { - if (contacts.size() != 1) { + if (members.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); return null; } - return new SingleChat(id, contacts.iterator().next(), xmppID, read, jsonViewSettings); + return new SingleChat(id, members.get(0).contact, xmppID, read, jsonViewSettings); } } @@ -344,8 +347,10 @@ static Map loadReceiver(int chatID) { return dbReceiver; } - public class KonChatState { - private final Contact mContact; + public static final class Member { + public final Contact contact; + public final GroupChat.Role role; + private ChatState mState = ChatState.gone; // note: the Android client does not set active states when only viewing // the chat (not necessary according to XEP-0085), this makes the @@ -353,12 +358,36 @@ public class KonChatState { // TODO save last active date to DB private Date mLastActive = null; - protected KonChatState(Contact contact) { - mContact = contact; + public Member(Contact contact){ + this(contact, Role.DEFAULT); + } + + public Member(Contact contact, GroupChat.Role role) { + this.contact = contact; + this.role = role; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Member)) + return false; + + return this.contact.equals(o); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(this.contact); + return hash; } - public Contact getContact() { - return mContact; + @Override + public String toString() { + return "Mem:c={"+contact+"}r="+role; } public ChatState getState() { diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index 989f9926..720d665a 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -20,10 +20,12 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; @@ -121,9 +123,9 @@ public Optional get(GroupMetaData gData) { public GroupChat getOrCreate(GroupMetaData gData, Contact contact) { GroupChat chat = this.get(gData, contact).orElse(null); if (chat != null) - return chat; + return chat;; - return this.createNew(new Contact[]{contact}, gData, ""); + return this.createNew(Arrays.asList(contact), gData, ""); } public Chat getOrCreate(Contact contact) { @@ -147,7 +149,7 @@ private SingleChat createNew(Contact contact, String xmppThreadID) { return newChat; } - public GroupChat createNew(Contact[] contacts, GroupMetaData gData, String subject) { + public GroupChat createNew(List contacts, GroupMetaData gData, String subject) { GroupChat newChat = GroupChat.create(contacts, gData, subject); LOGGER.config("new group chat: "+newChat); this.putSilent(newChat); @@ -157,7 +159,7 @@ public GroupChat createNew(Contact[] contacts, GroupMetaData gData, String subje private void putSilent(Chat chat) { if (mMap.containsValue(chat)) { - LOGGER.warning("chat already in chat list"); + LOGGER.warning("chat already in chat list: "+chat); return; } diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 918db3a6..e5d8f8d1 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -18,7 +18,7 @@ package org.kontalk.model; -import java.util.HashMap; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -38,26 +38,27 @@ public abstract class GroupChat extends Chat { private static final Logger LOGGER = Logger.getLogger(GroupChat.class.getName()); - private final HashMap mContactMap = new HashMap<>(); + //private final HashMap mContactMap = new HashMap<>(); + private final HashSet mMemberSet = new HashSet<>(); private final D mGroupData; private String mSubject; // TODO overwrite encryption=OFF field private boolean mForceEncryptionOff = false; - private GroupChat(Contact[] contacts, D gData, String subject) { - super(contacts, "", subject, gData); + private GroupChat(List members, D gData, String subject) { + super(members, "", subject, gData); mGroupData = gData; mSubject = subject; - for (Contact contact: contacts) - this.addContactSilent(contact); + for (Member member: members) + this.addMemberSilent(member); } // used when loading from database private GroupChat(int id, - Set contacts, + List members, D gData, String subject, boolean read, @@ -68,41 +69,49 @@ private GroupChat(int id, mGroupData = gData; mSubject = subject; - for (Contact contact: contacts) - this.addContactSilent(contact); + for (Member member: members) + this.addMemberSilent(member); } /** Get all contacts (including deleted and user contact). */ @Override - public Set getAllContacts() { - return new HashSet<>(mContactMap.keySet()); + public List getAllContacts() { + List l = new ArrayList<>(mMemberSet.size()); + for (Member m : mMemberSet) + l.add(m.contact); + + return l; } @Override public Contact[] getValidContacts() { //chat.getContacts().stream().filter(c -> !c.isDeleted()); Set contacts = new HashSet<>(); - for (Contact c : mContactMap.keySet()) { - if (!c.isDeleted() && !c.isMe()) { - contacts.add(c); + for (Member m : mMemberSet) { + if (!m.contact.isDeleted() && !m.contact.isMe()) { + contacts.add(m.contact); } } return contacts.toArray(new Contact[0]); } public void addContact(Contact contact) { - this.addContactSilent(contact); + this.addMemberSilent(new Member(contact)); this.save(); this.changed(contact); } public void addContacts(List contacts) { boolean changed = false; - for (Contact c: contacts) - if (!this.getAllContacts().contains(c)) { - this.addContactSilent(c); + for (Contact c: contacts) { + Member m = new Member(c); + if (!mMemberSet.contains(m)) { + this.addMemberSilent(m); changed = true; + } else { + LOGGER.info("contact already in chat: "+c); } + } if (changed) { System.out.println("addContacts save"); @@ -111,24 +120,23 @@ public void addContacts(List contacts) { } } - private void addContactSilent(Contact contact) { - if (mContactMap.containsKey(contact)) { - LOGGER.warning("contact already in chat: "+contact); + private void addMemberSilent(Member member) { + if (mMemberSet.contains(member)) { + LOGGER.warning("contact already in chat: "+member); return; } - contact.addObserver(this); - mContactMap.put(contact, new KonChatState(contact)); + member.contact.addObserver(this); + mMemberSet.add(member); } private void removeContactSilent(Contact contact) { contact.deleteObserver(this); - if (!mContactMap.containsKey(contact)) { + boolean succ = mMemberSet.remove(new Member(contact)); + if (!succ) { LOGGER.warning("contact not in chat: "+contact); return; } - - mContactMap.remove(contact); this.save(); } @@ -151,21 +159,28 @@ public void setSubject(String subject) { } @Override - public void setChatState(Contact contact, ChatState chatState) { - KonChatState state = mContactMap.get(contact); - if (state == null) { - LOGGER.warning("can't find contact in contact map!?"); + public void setChatState(final Contact contact, ChatState chatState) { + Member member = mMemberSet.stream().filter(new Predicate(){ + @Override + public boolean test(Member t) { + return t.contact.equals(contact); + } + }).findFirst().orElse(null); + + if (member == null) { + LOGGER.warning("can't find member in member set!?"); return; } - state.setState(chatState); - this.changed(state); + + member.setState(chatState); + this.changed(member); } public void applyGroupCommand(MessageContent.GroupCommand command, Contact sender) { switch(command.getOperation()) { case CREATE: - assert mContactMap.size() == 1; - assert mContactMap.containsKey(sender); + assert mMemberSet.size() == 1; + assert mMemberSet.contains(new Member(sender)); boolean meIn = false; for (JID jid: command.getAdded()) { @@ -175,12 +190,13 @@ public void applyGroupCommand(MessageContent.GroupCommand command, Contact sende continue; } - if (mContactMap.keySet().contains(contact)) { - LOGGER.warning("contact already in chat: "+contact); + Member member = new Member(contact); + if (mMemberSet.contains(member)) { + LOGGER.warning("member already in chat: "+member); continue; } meIn |= contact.isMe(); - this.addContactSilent(contact); + this.addMemberSilent(member); } if (!meIn) @@ -202,7 +218,7 @@ public void applyGroupCommand(MessageContent.GroupCommand command, Contact sende LOGGER.warning("can't get added contact, jid="+jid); continue; } - this.addContactSilent(contact); + this.addMemberSilent(new Member(contact)); } for (JID jid : command.getRemoved()) { Contact contact = ContactList.getInstance().get(jid).orElse(null); @@ -256,11 +272,11 @@ public boolean isAdministratable() { } private boolean containsMe() { - return mContactMap.keySet().parallelStream().anyMatch( - new Predicate() { + return mMemberSet.parallelStream().anyMatch( + new Predicate() { @Override - public boolean test(Contact t) { - return t.isMe(); + public boolean test(Member t) { + return t.contact.isMe(); } } ); @@ -268,7 +284,7 @@ public boolean test(Contact t) { @Override void save() { - this.save(mContactMap.keySet().toArray(new Contact[0]), mSubject); + this.save(new ArrayList<>(mMemberSet), mSubject); } @Override @@ -294,35 +310,39 @@ public String toString() { } public static final class KonGroupChat extends GroupChat { - KonGroupChat(Contact[] contacts, KonGroupData gData, String subject) { - super(contacts, gData, subject); + private KonGroupChat(List members, KonGroupData gData, String subject) { + super(members, gData, subject); } - KonGroupChat(int id, Set contacts, KonGroupData gData, String subject, boolean read, String jsonViewSettings) { - super(id, contacts, gData, subject, read, jsonViewSettings); + private KonGroupChat(int id, List members, KonGroupData gData, String subject, boolean read, String jsonViewSettings) { + super(id, members, gData, subject, read, jsonViewSettings); } } public static final class MUCChat extends GroupChat { - private MUCChat(Contact[] contacts, GroupMetaData.MUCData gData, String subject) { - super(contacts, gData, subject); + private MUCChat(List members, GroupMetaData.MUCData gData, String subject) { + super(members, gData, subject); } - private MUCChat(int id, Set contacts, GroupMetaData.MUCData gData, String subject, boolean read, String jsonViewSettings) { - super(id, contacts, gData, subject, read, jsonViewSettings); + private MUCChat(int id, List members, GroupMetaData.MUCData gData, String subject, boolean read, String jsonViewSettings) { + super(id, members, gData, subject, read, jsonViewSettings); } } - static GroupChat create(int id, Set contacts, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { + static GroupChat create(int id, List members, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { return (gData instanceof KonGroupData) ? - new KonGroupChat(id, contacts, (KonGroupData) gData, subject, read, jsonViewSettings) : - new MUCChat(id, contacts, (MUCData) gData, subject, read, jsonViewSettings); + new KonGroupChat(id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : + new MUCChat(id, members, (MUCData) gData, subject, read, jsonViewSettings); } - public static GroupChat create(Contact[] contacts, GroupMetaData gData, String subject) { + public static GroupChat create(List contacts, GroupMetaData gData, String subject) { + List members = new ArrayList<>(contacts.size()); + for (Contact c : contacts) { + members.add(new Member(c)); + } return (gData instanceof KonGroupData) ? - new KonGroupChat(contacts, (KonGroupData) gData, subject) : - new MUCChat(contacts, (MUCData) gData, subject); + new KonGroupChat(members, (KonGroupData) gData, subject) : + new MUCChat(members, (MUCData) gData, subject); } } diff --git a/src/main/java/org/kontalk/model/SingleChat.java b/src/main/java/org/kontalk/model/SingleChat.java index 5a0fe3e4..34ae426d 100644 --- a/src/main/java/org/kontalk/model/SingleChat.java +++ b/src/main/java/org/kontalk/model/SingleChat.java @@ -18,9 +18,9 @@ package org.kontalk.model; -import java.util.HashSet; +import java.util.Arrays; +import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; @@ -31,19 +31,16 @@ public final class SingleChat extends Chat { private static final Logger LOGGER = Logger.getLogger(SingleChat.class.getName()); - private final Contact mContact; + private final Member mMember; private final String mXMPPID; - private final KonChatState mChatState; SingleChat(Contact contact, String xmppID) { super(contact, xmppID, ""); - mContact = contact; + mMember = new Member(contact); contact.addObserver(this); // note: Kontalk Android client is ignoring the chat id mXMPPID = xmppID; - - mChatState = new KonChatState(contact); } // used when loading from database @@ -55,31 +52,27 @@ public final class SingleChat extends Chat { ) { super(id, read, jsonViewSettings); - mContact = contact; + mMember = new Member(contact); contact.addObserver(this); mXMPPID = xmppID; - - mChatState = new KonChatState(contact); } public Contact getContact() { - return mContact; + return mMember.contact; } @Override - public Set getAllContacts() { - Set contacts = new HashSet<>(); - contacts.add(mContact); - - return contacts; + public List getAllContacts() { + return Arrays.asList(mMember.contact); } @Override public Contact[] getValidContacts() { - if (mContact.isDeleted() || mContact.isBlocked() && !mContact.isMe()) + Contact c = mMember.contact; + if (c.isDeleted() || c.isBlocked() && !c.isMe()) return new Contact[0]; - return new Contact[]{mContact}; + return new Contact[]{c}; } @Override @@ -94,17 +87,18 @@ public String getSubject() { @Override public boolean isSendEncrypted() { - return mContact.getEncrypted(); + return mMember.contact.getEncrypted(); } @Override public boolean canSendEncrypted() { - return !mContact.isDeleted() && !mContact.isBlocked() && mContact.hasKey(); + Contact c = mMember.contact; + return !c.isDeleted() && !c.isBlocked() && c.hasKey(); } @Override public boolean isValid() { - return !mContact.isDeleted() && !mContact.isBlocked(); + return !mMember.contact.isDeleted() && !mMember.contact.isBlocked(); } @Override @@ -114,17 +108,17 @@ public boolean isAdministratable() { @Override public void setChatState(Contact contact, ChatState chatState) { - if (contact != mContact) { + if (!contact.equals(mMember.contact)) { LOGGER.warning("wrong contact!?"); return; } - mChatState.setState(chatState); - this.changed(mChatState); + mMember.setState(chatState); + this.changed(mMember.getState()); } @Override void save() { - super.save(new Contact[]{mContact}, ""); + super.save(Arrays.asList(mMember), ""); } @Override @@ -134,13 +128,13 @@ public boolean equals(Object o) { if (!(o instanceof SingleChat)) return false; SingleChat oChat = (SingleChat) o; - return mContact.equals(oChat.mContact) && mXMPPID.equals(oChat.mXMPPID); + return mMember.equals(oChat.mMember) && mXMPPID.equals(oChat.mXMPPID); } @Override public int hashCode() { int hash = 7; - hash = 41 * hash + Objects.hashCode(this.mContact); + hash = 41 * hash + Objects.hashCode(this.mMember); hash = 41 * hash + Objects.hashCode(this.mXMPPID); return hash; } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index be50ee15..373b6e19 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -704,7 +704,7 @@ public void createGroupChat(List contacts, String subject) { KonGroupData gData = GroupControl.newKonGroupData(me.getJID()); //MUCData gData = GroupControl.newMUCGroupData(); - GroupChat chat = ChatList.getInstance().createNew(withMe.toArray(new Contact[0]), + GroupChat chat = ChatList.getInstance().createNew(withMe, gData, subject); diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 0db95430..5947a4d0 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -39,7 +39,6 @@ import org.kontalk.system.Config; import org.kontalk.model.KonMessage; import org.kontalk.model.Chat; -import org.kontalk.model.Chat.KonChatState; import org.kontalk.model.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.GroupChat; @@ -259,15 +258,15 @@ private void updateView(Object arg) { } String stateText = ""; - if (arg instanceof Chat.KonChatState) { - KonChatState state = (KonChatState) arg; - switch(state.getState()) { + if (arg instanceof Chat.Member) { + Chat.Member member = (Chat.Member) arg; + switch(member.getState()) { case composing: stateText = Tr.tr("is writing…"); break; //case paused: activity = T/r.tr("stopped typing"); break; //case inactive: stateText = T/r.tr("is inactive"); break; } if (!stateText.isEmpty() && mValue.isGroupChat()) - stateText = state.getContact().getName() + ": " + stateText; + stateText = member.contact.getName() + ": " + stateText; } if (stateText.isEmpty()) { mChatStateLabel.setText(""); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 188b63fc..179159ad 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -128,7 +128,7 @@ protected void updateOnEDT(Object arg) { if (arg instanceof Set || arg instanceof String || arg instanceof Boolean || - arg instanceof Chat.KonChatState) { + arg instanceof Chat.Member) { // contacts, subject, read status or chat state changed, nothing // to do here return; diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 7f98fa50..f87a4aa0 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -226,7 +226,7 @@ static String chatTitle(Chat chat) { String subj = chat.getSubject(); return !subj.isEmpty() ? subj : Tr.tr("Group Chat"); } else { - return Utils.displayNames(new ArrayList<>(chat.getAllContacts())); + return Utils.displayNames(chat.getAllContacts()); } } From afa3e35bc824b223fe3fc1b2ea0c9580919f9cbd Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 16:00:38 +0100 Subject: [PATCH 104/257] added option for disabling avatar download; minimal solution; see #65 --- src/main/java/org/kontalk/client/Client.java | 8 +++++--- src/main/java/org/kontalk/system/AvatarHandler.java | 4 ++++ src/main/java/org/kontalk/system/Config.java | 2 ++ src/main/java/org/kontalk/view/ConfigurationDialog.java | 7 +++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index e09506de..2388d563 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -161,9 +161,11 @@ public void connect(PersonalKey key) { StanzaFilter presenceFilter = new StanzaTypeFilter(Presence.class); mConn.addAsyncStanzaListener(new PresenceListener(roster, rosterHandler), presenceFilter); - // our service discovery: want avatar from other users - ServiceDiscoveryManager.getInstanceFor(mConn). - addFeature(AvatarSendReceiver.NOTIFY_FEATURE); + if (config.getBoolean(Config.NET_REQUEST_AVATARS)) { + // our service discovery: want avatar from other users + ServiceDiscoveryManager.getInstanceFor(mConn). + addFeature(AvatarSendReceiver.NOTIFY_FEATURE); + } // listen to all IQ errors mConn.addAsyncStanzaListener(this, IQTypeFilter.ERROR); diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index c1a5bf08..3a2c70c3 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -51,6 +51,10 @@ public final class AvatarHandler { } public void onNotify(JID jid, String id) { + if (Config.getInstance().getBoolean(Config.NET_REQUEST_AVATARS)) + // disabled by user + return; + Contact contact = ContactList.getInstance().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index 91165104..43d1550c 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -58,6 +58,7 @@ public final class Config extends PropertiesConfiguration { public static final String NET_SEND_ROSTER_NAME = "net.roster_name"; public static final String NET_STATUS_LIST = "net.status_list"; public static final String NET_AUTO_SUBSCRIPTION = "net.auto_subscription"; + public static final String NET_REQUEST_AVATARS = "net.request_avatars"; public static final String MAIN_CONNECT_STARTUP = "main.connect_startup"; public static final String MAIN_TRAY = "main.tray"; public static final String MAIN_TRAY_CLOSE = "main.tray_close"; @@ -100,6 +101,7 @@ private Config(Path configFile) { map.put(NET_SEND_ROSTER_NAME, false); map.put(NET_STATUS_LIST, new String[]{DEFAULT_XMPP_STATUS}); map.put(NET_AUTO_SUBSCRIPTION, false); + map.put(NET_REQUEST_AVATARS, true); map.put(MAIN_CONNECT_STARTUP, true); map.put(MAIN_TRAY, true); map.put(MAIN_TRAY_CLOSE, false); diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 1895db94..d90bccb0 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -116,6 +116,7 @@ private class MainPanel extends WebPanel { private final WebCheckBox mTrayBox; private final WebCheckBox mCloseTrayBox; private final WebCheckBox mEnterSendsBox; + private final WebCheckBox mRequestAvatars; private final WebCheckBox mBGBox; private final WebFileChooserField mBGChooser; @@ -151,6 +152,11 @@ public void itemStateChanged(ItemEvent e) { mConf.getBoolean(Config.MAIN_ENTER_SENDS)); groupPanel.add(new GroupPanel(mEnterSendsBox, new WebSeparator())); + mRequestAvatars = createCheckBox(Tr.tr("Download avatar images"), + Tr.tr("Download contact avatar images"), + mConf.getBoolean(Config.NET_REQUEST_AVATARS)); + groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator())); + String bgPath = mConf.getString(Config.VIEW_CHAT_BG); mBGBox = createCheckBox(Tr.tr("Custom background:")+" ", "", @@ -177,6 +183,7 @@ private void saveConfiguration() { mView.updateTray(); mConf.setProperty(Config.MAIN_ENTER_SENDS, mEnterSendsBox.isSelected()); mView.setHotkeys(); + mConf.setProperty(Config.NET_REQUEST_AVATARS, mRequestAvatars.isSelected()); String bgPath; if (mBGBox.isSelected() && !mBGChooser.getSelectedFiles().isEmpty()) { bgPath = mBGChooser.getSelectedFiles().get(0).getAbsolutePath(); From ecec427696c9469ddcdea2bb0d193c1c143858a3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 17:16:25 +0100 Subject: [PATCH 105/257] view: support for port specification in URL link detection --- src/main/java/org/kontalk/view/LinkUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/LinkUtils.java b/src/main/java/org/kontalk/view/LinkUtils.java index 3ade6fe1..de4891cf 100644 --- a/src/main/java/org/kontalk/view/LinkUtils.java +++ b/src/main/java/org/kontalk/view/LinkUtils.java @@ -56,7 +56,7 @@ final class LinkUtils { private static final Pattern URL_PATTERN = Pattern.compile( "(http[s]?://)?" + // scheme "(\\w+(-+\\w+)*\\.)+" + // sub- and host-level(s) - "[a-zA]{2,}" + // TLD + "[a-z]{2,}(:[0-9]+)?" + // TLD and port "(/[^\\s?#/]*)*" + // path "(\\?[^\\s?#]*)*" + // query "(\\#[^\\s?#]*)*", // fragment From 28653f419528f1d26b8b431e3595ee2bbc611474 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 18:46:38 +0100 Subject: [PATCH 106/257] view: key UID added to account configuration page --- .../org/kontalk/view/ConfigurationDialog.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index d90bccb0..36e29154 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -47,6 +47,7 @@ import javax.swing.Box; import javax.swing.JFrame; import javax.swing.text.NumberFormatter; +import org.apache.commons.lang.StringUtils; import org.kontalk.system.Config; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; @@ -203,6 +204,7 @@ private class AccountPanel extends WebPanel { private final WebTextField mServerField; private final WebFormattedTextField mPortField; private final WebCheckBox mDisableCertBox; + private final WebTextArea mUserIDArea; private final WebTextArea mFingerprintArea; AccountPanel() { @@ -237,11 +239,19 @@ private class AccountPanel extends WebPanel { groupPanel.add(new GroupPanel(mDisableCertBox, new WebSeparator())); groupPanel.add(new WebSeparator(true, true)); + + mUserIDArea = new WebTextArea().setBoldFont(); + mUserIDArea.setEditable(false); + mUserIDArea.setOpaque(false); + groupPanel.add(new GroupPanel(View.GAP_DEFAULT, + new WebLabel(Tr.tr("Key user ID:")), + mUserIDArea)); + WebLabel fpLabel = new WebLabel(Tr.tr("Key fingerprint:")+" "); fpLabel.setAlignmentY(Component.TOP_ALIGNMENT); GroupPanel fpLabelPanel = new GroupPanel(false, fpLabel, Box.createGlue()); mFingerprintArea = Utils.createFingerprintArea(); - this.updateFingerprint(); + this.updateKey(); groupPanel.add(new GroupPanel(View.GAP_DEFAULT, fpLabelPanel, mFingerprintArea)); final WebButton passButton = new WebButton(getPassTitle()); @@ -260,7 +270,7 @@ public void actionPerformed(ActionEvent e) { @Override public void actionPerformed(ActionEvent e) { mView.showImportWizard(false); - AccountPanel.this.updateFingerprint(); + AccountPanel.this.updateKey(); passButton.setText(getPassTitle()); } }); @@ -268,7 +278,6 @@ public void actionPerformed(ActionEvent e) { this.add(groupPanel, BorderLayout.CENTER); - WebButton okButton = new WebButton(Tr.tr("Save & Connect")); okButton.addActionListener(new ActionListener() { @Override @@ -284,11 +293,17 @@ public void actionPerformed(ActionEvent e) { this.add(buttonPanel, BorderLayout.SOUTH); } - private void updateFingerprint() { + private void updateKey() { PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); + String uid = key != null ? key.getUserId() : null; + mUserIDArea.setText(uid != null ? + StringUtils.abbreviate(uid, 40) : + "- "+Tr.tr("no key loaded")+" -"); + if (uid != null) + TooltipManager.addTooltip(mUserIDArea, uid); mFingerprintArea.setText(key != null ? Utils.fingerprint(key.getFingerprint()) : - "- " + Tr.tr("no key loaded") + " -"); + "---"); } private void saveConfiguration() { From 9420f291aea446715835f5c03ec4b351ce336160 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 18:48:46 +0100 Subject: [PATCH 107/257] (auto)update translation strings --- src/main/resources/i18n/strings.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index c386da90..e261b3f7 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -241,3 +241,6 @@ s_84VP = Edit your profile s_4QXX = Your profile picture: s_1R7I = Profile s_O2UH = Set your user profile +s_4FTV = Download avatar images +s_YL1F = Download contact avatar images +s_6SC9 = Key user ID: From af37abeacd7dcfda3a82da7494766b14af361052 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 19:11:02 +0100 Subject: [PATCH 108/257] delete avatar if contact disabled it --- src/main/java/org/kontalk/model/Avatar.java | 4 ++++ src/main/java/org/kontalk/model/Contact.java | 11 +++++++++++ src/main/java/org/kontalk/system/AvatarHandler.java | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 37a3a2f7..23e4240c 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -175,6 +175,10 @@ public static void createDir() { LOGGER.info("created avatar directory"); } + public static Avatar deleted() { + return new Avatar(""); + } + private static String id(BufferedImage image) { byte[] imageData = imageData(image); return imageData != null ? DigestUtils.sha1Hex(imageData) : ""; diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 822cb8f8..b0856354 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -278,6 +278,17 @@ public void setAvatar(Avatar avatar) { this.changed(avatar); } + public void deleteAvatar() { + // delete old + if (mAvatar != null) + mAvatar.delete(); + + mAvatar = null; + this.save(); + + this.changed(Avatar.deleted()); + } + public boolean isMe() { return mJID.isMe(); } diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 3a2c70c3..7ad6243d 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -63,7 +63,7 @@ public void onNotify(JID jid, String id) { if (id.isEmpty()) { // contact disabled avatar publishing - // TODO + contact.deleteAvatar(); } Avatar avatar = contact.getAvatar().orElse(null); From 3fd8cd38e8d71cbdf4495740dbe34e74c63662a4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 29 Jan 2016 19:13:07 +0100 Subject: [PATCH 109/257] crypto: reworked decryptor --- .../java/org/kontalk/crypto/Decryptor.java | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index ee19ecf4..ce382d17 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -61,6 +61,7 @@ import org.xmlpull.v1.XmlPullParserException; /** + * Decrypt message content. Message parameter is internally changed by methods. * * @author Alexander Bikadorov {@literal } */ @@ -72,27 +73,36 @@ private static class DecryptionResult { Coder.Signing signing = Coder.Signing.UNKNOWN; } - private final DecryptMessage message; - private PersonalKey myKey = null; - private Optional senderKey; + private final DecryptMessage mMessage; + // nullable + private final PersonalKey mMyKey; + // nullable + private final PGPUtils.PGPCoderKey mSenderKey; Decryptor(DecryptMessage message) { - this.message = message; + mMessage = message; + + mMyKey = Coder.myKeyOrNull(); + + // if sender signing key not found -> decryption possible, signature + // verification will not be possible + mSenderKey = Coder.contactkey(message.getContact()).orElse(null); } // note: signing requires also encryption boolean decryptMessage() { - if (!message.isEncrypted()) { + if (!mMessage.isEncrypted()) { LOGGER.warning("message not encrypted"); return false; } - boolean loaded = this.loadKeys(); - if (!loaded) + if (mMyKey == null) { + mMessage.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); return false; + } // decrypt - String encryptedContent = message.getContent().getEncryptedContent(); + String encryptedContent = mMessage.getContent().getEncryptedContent(); if (encryptedContent.isEmpty()) { LOGGER.warning("no encrypted data in encrypted message"); } @@ -104,20 +114,21 @@ boolean decryptMessage() { try { decResult = decryptAndVerify(encryptedIn, plainOut, - myKey.getPrivateEncryptionKey(), - senderKey.isPresent() ? Optional.of(senderKey.get().signKey) : - Optional.empty()); + mMyKey.getPrivateEncryptionKey(), + mSenderKey != null ? + Optional.of(mSenderKey.signKey) : + Optional.empty()); } catch (IOException | PGPException ex) { LOGGER.log(Level.WARNING, "can't decrypt message", ex); return false; } EnumSet allErrors = decResult.errors; - message.setSigning(decResult.signing); + mMessage.setSigning(decResult.signing); // parse decrypted CPIM content - String myUID = myKey.getUserId(); - String senderUID = senderKey.isPresent() ? - senderKey.get().userID : + String myUID = mMyKey.getUserId(); + String senderUID = mSenderKey != null ? + mSenderKey.userID : null; String decrText = EncodingUtils.getString( plainOut.toByteArray(), @@ -125,12 +136,12 @@ boolean decryptMessage() { MessageContent content = this.parseCPIMOrNull(decrText, myUID, Optional.ofNullable(senderUID)); // set errors - message.setSecurityErrors(allErrors); + mMessage.setSecurityErrors(allErrors); if (content != null) { // everything went better than expected LOGGER.info("message decryption successful"); - message.setDecryptedContent(content); + mMessage.setDecryptedContent(content); return true; } else { LOGGER.warning("message decryption failed"); @@ -139,11 +150,12 @@ boolean decryptMessage() { } void decryptAttachment(Path baseDir) { - if (!(message instanceof InMessage)) { + // TODO ugly + if (!(mMessage instanceof InMessage)) { LOGGER.warning("message not incoming message"); return; } - InMessage inMessage = (InMessage) message; + InMessage inMessage = (InMessage) mMessage; MessageContent.Attachment attachment = inMessage.getContent().getAttachment().orElse(null); if (attachment == null) { @@ -151,9 +163,10 @@ void decryptAttachment(Path baseDir) { return; } - boolean loaded = this.loadKeys(); - if (!loaded) + if (mMyKey == null) { + mMessage.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); return; + } // in file File inFile = baseDir.resolve(attachment.getFile()).toFile(); @@ -172,8 +185,8 @@ void decryptAttachment(Path baseDir) { FileOutputStream plainOut = new FileOutputStream(outFile)) { decResult = decryptAndVerify(encryptedIn, plainOut, - myKey.getPrivateEncryptionKey(), - senderKey.isPresent() ? Optional.of(senderKey.get().signKey) : + mMyKey.getPrivateEncryptionKey(), + mSenderKey != null ? Optional.of(mSenderKey.signKey) : Optional.empty()); } catch (IOException | PGPException ex){ LOGGER.log(Level.WARNING, "can't decrypt attachment", ex); @@ -188,22 +201,6 @@ void decryptAttachment(Path baseDir) { LOGGER.info("attachment decryption successful"); } - private boolean loadKeys() { - myKey = Coder.myKeyOrNull(); - if (myKey == null) { - message.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); - return false; - } - senderKey = Coder.contactkey(message.getContact()); - - // sender signing key not found -> continue decryption, siging will not - // be possible - if (senderKey.isPresent()) - message.setSecurityErrors(EnumSet.of(Coder.Error.KEY_UNAVAILABLE)); - - return true; - } - /** Decrypt, verify and write input stream data to output stream. */ private static DecryptionResult decryptAndVerify( InputStream encryptedInput, OutputStream plainOutput, @@ -361,7 +358,7 @@ private MessageContent parseCPIMOrNull(String cpim, cpimMessage = CPIMMessage.parse(cpim); } catch (ParseException ex) { LOGGER.log(Level.WARNING, "can't find valid CPIM data", ex); - message.setSecurityErrors(EnumSet.of(Coder.Error.INVALID_DATA)); + mMessage.setSecurityErrors(EnumSet.of(Coder.Error.INVALID_DATA)); return null; } @@ -400,7 +397,7 @@ private MessageContent parseCPIMOrNull(String cpim, } catch (XmlPullParserException | IOException | SmackException ex) { LOGGER.log(Level.WARNING, "can't parse XMPP XML string", ex); errors.add(Coder.Error.INVALID_DATA); - message.setSecurityErrors(errors); + mMessage.setSecurityErrors(errors); return null; } LOGGER.config("decrypted XML: "+m.toXML()); @@ -410,7 +407,7 @@ private MessageContent parseCPIMOrNull(String cpim, decryptedContent = MessageContent.plainText(content); } - message.setSecurityErrors(errors); + mMessage.setSecurityErrors(errors); return decryptedContent; } } From c96d68f5bc8a901b19cf1ae39176833bae5016ee Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 30 Jan 2016 15:25:30 +0100 Subject: [PATCH 110/257] view: new option for hiding user contact in contact list view --- src/main/java/org/kontalk/model/ContactList.java | 11 +++++++++-- src/main/java/org/kontalk/system/Config.java | 2 ++ src/main/java/org/kontalk/view/ComponentUtils.java | 7 +------ .../java/org/kontalk/view/ConfigurationDialog.java | 8 ++++++++ src/main/java/org/kontalk/view/ContactListView.java | 2 +- src/main/java/org/kontalk/view/Utils.java | 8 ++++++++ src/main/java/org/kontalk/view/View.java | 4 ++++ 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 47074f23..3de3e12a 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -28,8 +28,10 @@ import java.util.Observable; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.kontalk.system.Database; /** @@ -125,8 +127,13 @@ public Optional getMe() { return this.create(myJID, ""); } - public Set getAll() { - return new HashSet<>(mJIDMap.values()); + public Set getAll(final boolean withMe) { + return new HashSet<>(mJIDMap.values().stream().filter(new Predicate(){ + @Override + public boolean test(Contact t) { + return withMe || !t.isMe(); + } + }).collect(Collectors.toSet())); } public void delete(Contact contact) { diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index 43d1550c..958069bd 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -54,6 +54,7 @@ public final class Config extends PropertiesConfiguration { public static final String VIEW_FRAME_HEIGHT = "view.frame.height"; public static final String VIEW_SELECTED_CHAT = "view.thread"; public static final String VIEW_CHAT_BG = "view.thread_bg"; + public static final String VIEW_USER_CONTACT = "view.user_in_contactlist"; public static final String NET_SEND_CHAT_STATE = "net.chatstate"; public static final String NET_SEND_ROSTER_NAME = "net.roster_name"; public static final String NET_STATUS_LIST = "net.status_list"; @@ -97,6 +98,7 @@ private Config(Path configFile) { map.put(VIEW_FRAME_HEIGHT, 650); map.put(VIEW_SELECTED_CHAT, -1); map.put(VIEW_CHAT_BG, ""); + map.put(VIEW_USER_CONTACT, false); map.put(NET_SEND_CHAT_STATE, true); map.put(NET_SEND_ROSTER_NAME, false); map.put(NET_STATUS_LIST, new String[]{DEFAULT_XMPP_STATUS}); diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index adab9b3b..3fc57839 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -64,7 +64,6 @@ import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.Set; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.DefaultListModel; @@ -87,7 +86,6 @@ import javax.swing.text.PlainDocument; import org.kontalk.misc.JID; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.system.Config; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; @@ -376,9 +374,8 @@ private void createGroup() { @Override void onShow() { - Set allContacts = ContactList.getInstance().getAll(); List contacts = new LinkedList<>(); - for (Contact c : allContacts) { + for (Contact c : Utils.allContacts()) { if (c.isKontalkUser() && !c.isMe()) contacts.add(c); } @@ -643,7 +640,6 @@ static class ModalPopup extends WebPopup { private final WebPanel layerPanel; ModalPopup(AbstractButton invokerButton) { - super(); mInvoker = invokerButton; layerPanel = new WebPanel(); @@ -744,7 +740,6 @@ static class TextLimitDocument extends PlainDocument { private final int mLimit; TextLimitDocument(int limit) { - super(); this.mLimit = limit; } diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 36e29154..546c8699 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -118,6 +118,7 @@ private class MainPanel extends WebPanel { private final WebCheckBox mCloseTrayBox; private final WebCheckBox mEnterSendsBox; private final WebCheckBox mRequestAvatars; + private final WebCheckBox mUserContact; private final WebCheckBox mBGBox; private final WebFileChooserField mBGChooser; @@ -158,6 +159,11 @@ public void itemStateChanged(ItemEvent e) { mConf.getBoolean(Config.NET_REQUEST_AVATARS)); groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator())); + mUserContact = createCheckBox(Tr.tr("Show yourself in contacts"), + Tr.tr("Show yourself in the contact list"), + mConf.getBoolean(Config.VIEW_USER_CONTACT)); + groupPanel.add(new GroupPanel(mUserContact, new WebSeparator())); + String bgPath = mConf.getString(Config.VIEW_CHAT_BG); mBGBox = createCheckBox(Tr.tr("Custom background:")+" ", "", @@ -185,6 +191,8 @@ private void saveConfiguration() { mConf.setProperty(Config.MAIN_ENTER_SENDS, mEnterSendsBox.isSelected()); mView.setHotkeys(); mConf.setProperty(Config.NET_REQUEST_AVATARS, mRequestAvatars.isSelected()); + mConf.setProperty(Config.VIEW_USER_CONTACT, mUserContact.isSelected()); + mView.updateContactList(); String bgPath; if (mBGBox.isSelected() && !mBGChooser.getSelectedFiles().isEmpty()) { bgPath = mBGChooser.getSelectedFiles().get(0).getAbsolutePath(); diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index ac254c3e..960e7840 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -88,7 +88,7 @@ public void mouseClicked(MouseEvent e) { @Override protected void updateOnEDT(Object arg) { Set newItems = new HashSet<>(); - Set contacts = mContactList.getAll(); + Set contacts = Utils.allContacts(); for (Contact contact: contacts) if (!this.containsValue(contact)) newItems.add(new ContactItem(contact)); diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index f87a4aa0..9620eef3 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -46,6 +46,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLHandshakeException; @@ -59,6 +60,8 @@ import org.kontalk.misc.KonException; import org.kontalk.model.Chat; import org.kontalk.model.Contact; +import org.kontalk.model.ContactList; +import org.kontalk.system.Config; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; import org.ocpsoft.prettytime.PrettyTime; @@ -359,6 +362,11 @@ static boolean confirmDeletion(Component parent, String text) { return selectedOption == WebOptionPane.OK_OPTION; } + static Set allContacts() { + boolean showMe = Config.getInstance().getBoolean(Config.VIEW_USER_CONTACT); + return ContactList.getInstance().getAll(showMe); + } + static List contactList(Chat chat) { List contacts = new ArrayList<>(chat.getAllContacts()); contacts.sort(new Comparator() { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index a2afda76..93b6f8fc 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -390,6 +390,10 @@ void reloadChatBG() { mChatView.loadDefaultBG(); } + void updateContactList() { + mContactListView.updateOnEDT(null); + } + void updateTray() { mTrayManager.setTray(); } From 9b3b6800a7b9b67aead0b6d71df33eb4e2daf9f0 Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Thu, 28 Jan 2016 17:43:41 +0000 Subject: [PATCH 111/257] Translated using Weblate (German) Currently translated at 98.3% (234 of 238 strings) --- src/main/resources/i18n/strings_de.properties | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index a501bff5..c5aea8c7 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -291,3 +291,11 @@ s_PC1G=schreibt… s_KRQ6=Passwort eingeben… s_2R2P=lade… s_307W=herunterladen… +s_68LN=Anfrage +s_29CW=Kontakt bearbeiten +s_AGYA=Kontakteinstellungen bearbeiten +s_MNIJ=Warte… +s_0WUC=Nicht überprüft +s_FDTC=Wenn Sie dies aktzeptieren, wird dieser Kontakt Ihren Onlinestatus sehen können. +s_MFZY=Onlinestatusanfragen von anderen Nutzern automatisch genehmigen +s_FAS6=Chataktivität (schreibt…) an andere Nutzer senden From 2ede01fc763cafb2bc5f633e7758cba86687f093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Ne=C4=8Das?= Date: Sat, 30 Jan 2016 21:08:33 +0000 Subject: [PATCH 112/257] Translated using Weblate (Czech) Currently translated at 100.0% (245 of 245 strings) --- src/main/resources/i18n/strings_cs.properties | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/resources/i18n/strings_cs.properties b/src/main/resources/i18n/strings_cs.properties index c288e049..048adbf5 100644 --- a/src/main/resources/i18n/strings_cs.properties +++ b/src/main/resources/i18n/strings_cs.properties @@ -261,3 +261,22 @@ s_KRQ6=Zadejte heslo… s_2R2P=načítání… s_307W=stahování… s_4WC0=Nepodařilo se ověřit certifikát serveru. +s_68LN=Požadavek +s_7MDX=Požádat o autorizaci získání stavu připojení od uživatele +s_OTEU=Automaticky autorizovat +s_MFZY=Automaticky autorizovat požadavky na stav připojení od jiných uživatelů +s_FAS6=Odesílat aktivitu v konverzaci (píše…) ostatním uživatelům +s_29CW=Upravit kontakt +s_AGYA=Upravit nastavení kontaktu +s_MNIJ=Čekám… +s_0WUC=Neověřeno +s_9S4L=Požadavek na autorizaci +s_FDTC=Pokud přijmete, tento kontakt uvidí váš stav online. +s_TAG1=Uživatelský profil +s_84VP=Upravit váš profil +s_4QXX=Váš profilový obrázek: +s_1R7I=Profil +s_O2UH=Nastavit váš uživatelský profil +s_4FTV=Stahovat obrázky avatarů +s_YL1F=Stahovat obrázky avatarů kontaktů +s_6SC9=ID klíčového uživatele: From 907ed917724f1e9772ae9be018e46f4df4612630 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 31 Jan 2016 19:56:37 +0100 Subject: [PATCH 113/257] view: improved ui strings --- src/main/java/org/kontalk/util/Tr.java | 3 +-- src/main/java/org/kontalk/view/ConfigurationDialog.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/util/Tr.java b/src/main/java/org/kontalk/util/Tr.java index 111db845..e188f79c 100644 --- a/src/main/java/org/kontalk/util/Tr.java +++ b/src/main/java/org/kontalk/util/Tr.java @@ -48,7 +48,6 @@ public class Tr { private static final String WIKI_HOME = "Home"; private static final List WIKI_LANGS = Arrays.asList("de"); - /** Map default (English) strings to translated strings. **/ private static Map TR_MAP = null; @@ -56,7 +55,7 @@ public class Tr { * Translate string used in user interface. * Spaces at beginning or end of string not supported! * @param s string that wants to be translated (in English) - * @return translation of input string (depending of platform language) + * @return translation of input string (depending on platform language) */ public static String tr(String s) { if (TR_MAP == null || !TR_MAP.containsKey(s)) diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 546c8699..5709c266 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -154,8 +154,8 @@ public void itemStateChanged(ItemEvent e) { mConf.getBoolean(Config.MAIN_ENTER_SENDS)); groupPanel.add(new GroupPanel(mEnterSendsBox, new WebSeparator())); - mRequestAvatars = createCheckBox(Tr.tr("Download avatar images"), - Tr.tr("Download contact avatar images"), + mRequestAvatars = createCheckBox(Tr.tr("Download profile pictures"), + Tr.tr("Download contact profile pictures"), mConf.getBoolean(Config.NET_REQUEST_AVATARS)); groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator())); From 59c2137bda95bd2a554455cc8916a890ff71d8bd Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 31 Jan 2016 19:59:09 +0100 Subject: [PATCH 114/257] (auto)update translation strings --- src/main/resources/i18n/strings.properties | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index e261b3f7..b952b6ed 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -241,6 +241,8 @@ s_84VP = Edit your profile s_4QXX = Your profile picture: s_1R7I = Profile s_O2UH = Set your user profile -s_4FTV = Download avatar images -s_YL1F = Download contact avatar images s_6SC9 = Key user ID: +s_6K2I = Download profile pictures +s_08GL = Download contact profile pictures +s_T2TO = Show yourself in contacts +s_F1X0 = Show yourself in the contact list From 6c5c666f72461776e1c4821778710927a273162d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 14:28:46 +0100 Subject: [PATCH 115/257] i18n: new language: Portuguese --- src/main/resources/i18n/strings_pt.properties | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/i18n/strings_pt.properties diff --git a/src/main/resources/i18n/strings_pt.properties b/src/main/resources/i18n/strings_pt.properties new file mode 100644 index 00000000..e69de29b From 37efd618fb7fda20c8bb7524deae20514824fd65 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 14:44:07 +0100 Subject: [PATCH 116/257] extended user avatar --- .../kontalk/client/AvatarSendReceiver.java | 11 ++++ src/main/java/org/kontalk/client/Client.java | 21 ++++++- src/main/java/org/kontalk/model/Avatar.java | 5 ++ src/main/java/org/kontalk/system/Control.java | 19 +++--- .../java/org/kontalk/view/ProfileDialog.java | 62 ++++++++++++++++--- 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index 6f89b69d..51f9efd2 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -110,6 +110,17 @@ void publish(String id, byte[] data) { // publish meta data... } + boolean delete() { + if (!mConn.isAuthenticated()) { + LOGGER.info("not logged in"); + return false; + } + + // TODO + LOGGER.warning("not implemented"); + return false; + } + void processMetadataEvent(JID jid, ItemsExtension itemsExt) { List items = itemsExt.getItems(); if (items.isEmpty()) { diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 2388d563..f63af07a 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -462,9 +462,28 @@ public void publishAvatar(String id, byte[] data) { LOGGER.warning("no avatar sender"); return; } - mAvatarSendReceiver.publish(id, data); + if (mFeatures.contains(Client.ServerFeature.PUBSUB)) { + mAvatarSendReceiver.publish(id, data); + } else { + LOGGER.info("not supported by server"); + } } + public boolean deleteAvatar() { + if (mAvatarSendReceiver == null) { + LOGGER.warning("no avatar sender"); + return false; + } + + if (mFeatures.contains(Client.ServerFeature.PUBSUB)) { + return mAvatarSendReceiver.delete(); + } else { + LOGGER.info("not supported by server"); + return false; + } + } + + // TODO unused public EnumSet getServerFeatures() { if (!this.isConnected()) mFeatures.clear(); diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 23e4240c..0d7f06f1 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -167,6 +167,11 @@ public static UserAvatar setImage(BufferedImage image) { USER_AVATAR = new UserAvatar(image); return USER_AVATAR; } + + public static void deleteImage() { + USER_AVATAR.delete(); + USER_AVATAR = new UserAvatar(); + } } public static void createDir() { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 373b6e19..d38e1ba4 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -754,17 +754,22 @@ public void sendAttachment(Chat chat, Path file){ this.sendTextMessage(chat, "", file); } - public Optional getUserAvatar() { - return Avatar.UserAvatar.instance().loadImage(); + public void setUserAvatar(BufferedImage image) { + Avatar.UserAvatar newAvatar = Avatar.UserAvatar.setImage(image); + byte[] avatarData = newAvatar.imageData().orElse(null); + if (avatarData == null || newAvatar.getID().isEmpty()) + return; + + mClient.publishAvatar(newAvatar.getID(), avatarData); } - public void setUserAvatar(BufferedImage image) { - Avatar.UserAvatar avatar = Avatar.UserAvatar.setImage(image); - byte[] avatarData = avatar.imageData().orElse(null); - if (avatarData == null || avatar.getID().isEmpty()) + public void unsetUserAvatar(){ + if (Avatar.UserAvatar.instance().getID().isEmpty()) return; - mClient.publishAvatar(avatar.getID(), avatarData); + boolean succ = mClient.deleteAvatar(); + if (succ) + Avatar.UserAvatar.deleteImage(); } /* private */ diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index f780d866..1b41d983 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -24,6 +24,8 @@ import com.alee.laf.filechooser.WebFileChooser; import com.alee.laf.label.WebLabel; import com.alee.laf.list.WebList; +import com.alee.laf.menu.WebMenuItem; +import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.rootpane.WebDialog; import com.alee.laf.scroll.WebScrollPane; import com.alee.laf.separator.WebSeparator; @@ -43,6 +45,8 @@ import java.util.List; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import org.apache.commons.lang.ObjectUtils; +import org.kontalk.model.Avatar; import org.kontalk.system.Config; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; @@ -58,9 +62,12 @@ final class ProfileDialog extends WebDialog { private final View mView; private final WebFileChooser mImgChooser; private final WebImage mAvatarImage; + private final BufferedImage mOldImage; private final WebTextField mStatusField; private final WebList mStatusList; + private BufferedImage mNewImage = null; + ProfileDialog(View view) { mView = view; @@ -78,21 +85,33 @@ final class ProfileDialog extends WebDialog { mImgChooser.setFileFilter(new ImageFilesFilter()); groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); - BufferedImage avatar = mView.getControl().getUserAvatar().orElse(null); - mAvatarImage = new WebImage(avatar != null ? - avatar : - AvatarLoader.createFallback(AVATAR_SIZE)); + + mAvatarImage = new WebImage(); + mOldImage = mNewImage = Avatar.UserAvatar.instance().loadImage().orElse(null); + this.setImage(mOldImage); //mAvatarImage.setDisplayType(DisplayType.fitComponent); //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); mAvatarImage.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + check(e); + } + @Override + public void mouseReleased(MouseEvent e) { + check(e); + } + private void check(MouseEvent e) { + if (e.isPopupTrigger()) { + ProfileDialog.this.showPopupMenu(e); + } + } @Override public void mouseClicked(MouseEvent e) { - if (!e.isPopupTrigger()) { + if (e.getButton() == MouseEvent.BUTTON1) { ProfileDialog.this.chooseAvatar(); } } - }); groupPanel.add(mAvatarImage); groupPanel.add(new WebSeparator(true, true)); @@ -151,6 +170,12 @@ public void actionPerformed(ActionEvent e) { this.pack(); } + private void setImage(BufferedImage avatar) { + mAvatarImage.setImage(avatar != null ? + avatar : + AvatarLoader.createFallback(AVATAR_SIZE)); + } + private void chooseAvatar() { int state = mImgChooser.showOpenDialog(this); if (state != WebFileChooser.APPROVE_OPTION) @@ -164,16 +189,33 @@ private void chooseAvatar() { if (img == null) return; - mAvatarImage.setImage(ImageUtils.createPreviewImage(img, AVATAR_SIZE)); + mNewImage = ImageUtils.createPreviewImage(img, AVATAR_SIZE); + mAvatarImage.setImage(mNewImage); } private void save() { - BufferedImage avatar = mAvatarImage.getImage(); - if (avatar != null) { - mView.getControl().setUserAvatar(avatar); + if (!ObjectUtils.equals(mOldImage, mNewImage)) { + if (mNewImage != null) { + mView.getControl().setUserAvatar(mNewImage); + } else { + mView.getControl().unsetUserAvatar(); + } } mView.getControl().setStatusText(mStatusField.getText()); } + private void showPopupMenu(MouseEvent e) { + WebPopupMenu menu = new WebPopupMenu(); + WebMenuItem removeItem = new WebMenuItem(Tr.tr("Remove")); + removeItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + ProfileDialog.this.setImage(mNewImage = null); + } + }); + removeItem.setEnabled(mNewImage != null); + menu.add(removeItem); + menu.show(mAvatarImage, e.getX(), e.getY()); + } } From 6fcc2f73232cdbf36bd50057316c126428c44ebe Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 16:37:19 +0100 Subject: [PATCH 117/257] reworked status change notification and added server features to event --- src/main/java/org/kontalk/client/Client.java | 33 +++++++++++-------- .../kontalk/client/KonConnectionListener.java | 12 +++---- src/main/java/org/kontalk/misc/ViewEvent.java | 12 ++++++- .../org/kontalk/system/AttachmentManager.java | 4 +-- src/main/java/org/kontalk/system/Control.java | 29 +++++++--------- .../org/kontalk/view/ContactListView.java | 2 +- src/main/java/org/kontalk/view/View.java | 25 ++++++++++---- 7 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index f63af07a..79bcd1fd 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -114,7 +114,7 @@ public void connect(PersonalKey key) { this.disconnect(); LOGGER.config("connecting..."); - mControl.setStatus(Control.Status.CONNECTING); + this.newStatus(Control.Status.CONNECTING); Config config = Config.getInstance(); //String network = config.getString(KonConf.SERV_NET); @@ -131,7 +131,7 @@ public void connect(PersonalKey key) { validateCertificate); // connection listener - mConn.addConnectionListener(new KonConnectionListener(mControl)); + mConn.addConnectionListener(new KonConnectionListener(this)); Roster roster = Roster.getInstanceFor(mConn); // subscriptions handled by roster handler @@ -167,6 +167,9 @@ public void connect(PersonalKey key) { addFeature(AvatarSendReceiver.NOTIFY_FEATURE); } + // listen to all acks + mConn.addStanzaAcknowledgedListener(new AcknowledgedListener(mControl)); + // listen to all IQ errors mConn.addAsyncStanzaListener(this, IQTypeFilter.ERROR); @@ -183,8 +186,8 @@ private void connectAsync() { mConn.connect(); } catch (XMPPException | SmackException | IOException ex) { LOGGER.log(Level.WARNING, "can't connect to "+mConn.getServer(), ex); - mControl.setStatus(Control.Status.FAILED); - mControl.handleException(new KonException(KonException.Error.CLIENT_CONNECT, ex)); + this.newStatus(Control.Status.FAILED); + mControl.onException(new KonException(KonException.Error.CLIENT_CONNECT, ex)); return; } @@ -194,14 +197,12 @@ private void connectAsync() { } catch (XMPPException | SmackException | IOException ex) { LOGGER.log(Level.WARNING, "can't login on "+mConn.getServer(), ex); mConn.disconnect(); - mControl.setStatus(Control.Status.FAILED); - mControl.handleException(new KonException(KonException.Error.CLIENT_LOGIN, ex)); + this.newStatus(Control.Status.FAILED); + mControl.onException(new KonException(KonException.Error.CLIENT_LOGIN, ex)); return; } } - mConn.addStanzaAcknowledgedListener(new AcknowledgedListener(mControl)); - // (server) service discovery, XEP-0030 // NOTE: smack automatically creates instances of SDM and CapsM and connects them ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn); @@ -271,12 +272,13 @@ private void connectAsync() { this.sendBlocklistRequest(); - mControl.setStatus(Control.Status.CONNECTED); + this.newStatus(Control.Status.CONNECTED); } public void disconnect() { synchronized (this) { if (mConn != null && mConn.isConnected()) { + this.newStatus(Control.Status.DISCONNECTING); mConn.disconnect(); } } @@ -483,12 +485,17 @@ public boolean deleteAvatar() { } } - // TODO unused - public EnumSet getServerFeatures() { - if (!this.isConnected()) + /* package internal*/ + + void newStatus(Control.Status status) { + if (status != Control.Status.CONNECTED) mFeatures.clear(); - return mFeatures.clone(); + mControl.onStatusChange(status, mFeatures.clone()); + } + + void newException(KonException konException) { + mControl.onException(konException); } @Override diff --git a/src/main/java/org/kontalk/client/KonConnectionListener.java b/src/main/java/org/kontalk/client/KonConnectionListener.java index f0f7d4b5..b622ccee 100644 --- a/src/main/java/org/kontalk/client/KonConnectionListener.java +++ b/src/main/java/org/kontalk/client/KonConnectionListener.java @@ -34,11 +34,11 @@ final class KonConnectionListener implements ConnectionListener { private static final Logger LOGGER = Logger.getLogger(KonConnectionListener.class.getName()); - private final Control mControl; + private final Client mClient; private boolean mConnected = false; - KonConnectionListener(Control control) { - mControl = control; + KonConnectionListener(Client client) { + mClient = client; } @Override @@ -58,7 +58,7 @@ public void authenticated(XMPPConnection connection, boolean resumed) { public void connectionClosed() { mConnected = false; LOGGER.info("connection closed"); - mControl.setStatus(Control.Status.DISCONNECTED); + mClient.newStatus(Control.Status.DISCONNECTED); } @Override @@ -71,8 +71,8 @@ public void connectionClosedOnError(Exception ex) { return; LOGGER.log(Level.WARNING, "connection closed on error", ex); - mControl.setStatus(Control.Status.ERROR); - mControl.handleException(new KonException(KonException.Error.CLIENT_ERROR, ex)); + mClient.newStatus(Control.Status.ERROR); + mClient.newException(new KonException(KonException.Error.CLIENT_ERROR, ex)); } @Override diff --git a/src/main/java/org/kontalk/misc/ViewEvent.java b/src/main/java/org/kontalk/misc/ViewEvent.java index f79f7f87..e6c41479 100644 --- a/src/main/java/org/kontalk/misc/ViewEvent.java +++ b/src/main/java/org/kontalk/misc/ViewEvent.java @@ -18,10 +18,13 @@ package org.kontalk.misc; +import java.util.EnumSet; +import org.kontalk.client.Client; import org.kontalk.crypto.PGPUtils.PGPCoderKey; import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; import org.kontalk.model.Contact; +import org.kontalk.system.Control; import org.kontalk.system.RosterHandler; /** @@ -33,7 +36,14 @@ public class ViewEvent { private ViewEvent() {} /** Application status changed. */ - public static class StatusChanged extends ViewEvent { + public static class StatusChange extends ViewEvent { + public final Control.Status status; + public final EnumSet features; + + public StatusChange(Control.Status status, EnumSet features) { + this.status = status; + this.features = features; + } } /** Key is password protected (ask for password). */ diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 106555d4..00ac5053 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -162,7 +162,7 @@ private void uploadAsync(OutMessage message) { } catch (KonException ex) { LOGGER.warning("upload failed, attachment: "+attachment); message.setStatus(KonMessage.Status.ERROR); - mControl.handleException(ex); + mControl.onException(ex); return; } @@ -207,7 +207,7 @@ public void updateProgress(int p) { path = client.download(attachment.getURL(), mAttachmentDir, listener); } catch (KonException ex) { LOGGER.warning("download failed, URL="+attachment.getURL()); - mControl.handleException(ex); + mControl.onException(ex); return; } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index d38e1ba4..569f68f4 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -89,7 +89,7 @@ public enum Status { private final AvatarHandler mAvatarHandler; private final GroupControl mGroupControl; - private Status mCurrentStatus = Status.DISCONNECTED; + private boolean mShuttingDown = false; private Control() { mViewControl = new ViewControl(); @@ -116,9 +116,8 @@ ViewControl getViewControl() { /* events from network client */ - public void setStatus(Status status) { - mCurrentStatus = status; - mViewControl.changed(new ViewEvent.StatusChanged()); + public void onStatusChange(Status status, EnumSet features) { + mViewControl.changed(new ViewEvent.StatusChange(status, features)); if (status == Status.CONNECTED) { String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); @@ -140,7 +139,7 @@ public void setStatus(Status status) { } } - public void handleException(KonException ex) { + public void onException(KonException ex) { mViewControl.changed(new ViewEvent.Exception(ex)); } @@ -552,14 +551,17 @@ public void launch() { } public void shutDown(boolean exit) { - if (mCurrentStatus == Status.SHUTTING_DOWN) + if (mShuttingDown) // we were already here return; - this.disconnect(); + mShuttingDown = true; + LOGGER.info("Shutting down..."); - mCurrentStatus = Status.SHUTTING_DOWN; - this.changed(new ViewEvent.StatusChanged()); + this.disconnect(); + + this.changed(new ViewEvent.StatusChange(Status.SHUTTING_DOWN, + EnumSet.noneOf(Client.ServerFeature.class))); try { Database.getInstance().close(); } catch (RuntimeException ex) { @@ -587,22 +589,15 @@ public void connect(char[] password) { public void disconnect() { mChatStateManager.imGone(); - mCurrentStatus = Status.DISCONNECTING; - this.changed(new ViewEvent.StatusChanged()); mClient.disconnect(); } - public Status getCurrentStatus() { - return mCurrentStatus; - } - public void setStatusText(String newStatus) { Config conf = Config.getInstance(); String[] strings = conf.getStringArray(Config.NET_STATUS_LIST); List stats = new ArrayList<>(Arrays.asList(strings)); stats.remove(newStatus); - stats.add(0, newStatus); if (stats.size() > 20) @@ -808,7 +803,7 @@ private PersonalKey keyOrNull(char[] password) { return account.load(password); } catch (KonException ex) { // something wrong with the account, tell view - Control.this.handleException(ex); + Control.this.onException(ex); return null; } } diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 960e7840..9c6c33cc 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -159,7 +159,7 @@ public void actionPerformed(ActionEvent event) { unblockItem.setVisible(false); } - Control.Status status = mView.getCurrentStatus(); + Control.Status status = mView.currentStatus(); boolean connected = status == Control.Status.CONNECTED; blockItem.setEnabled(connected); unblockItem.setEnabled(connected); diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 93b6f8fc..3410d8b5 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -40,8 +40,10 @@ import javax.swing.ToolTipManager; import java.awt.BorderLayout; import java.awt.Dimension; +import java.util.EnumSet; import java.util.concurrent.ExecutionException; import java.util.logging.Level; +import org.kontalk.client.Client; import org.kontalk.system.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Chat; @@ -104,6 +106,9 @@ public final class View implements Observer { private final WebStatusLabel mStatusBarLabel; private final MainFrame mMainFrame; + private Control.Status mCurrentStatus; + private EnumSet mServerFeatures; + private View(ViewControl control) { mControl = control; @@ -148,7 +153,7 @@ private View(ViewControl control) { // notifier mNotifier = new Notifier(this); - this.statusChanged(); + this.statusChanged(Control.Status.DISCONNECTED, EnumSet.noneOf(Client.ServerFeature.class)); } void setHotkeys() { @@ -171,8 +176,12 @@ public void run() { }); } - Control.Status getCurrentStatus() { - return mControl.getCurrentStatus(); + Control.Status currentStatus() { + return mCurrentStatus; + } + + EnumSet serverFeatures() { + return mServerFeatures; } void showConfig() { @@ -197,8 +206,9 @@ public void run() { } private void updateOnEDT(Object arg) { - if (arg instanceof ViewEvent.StatusChanged) { - this.statusChanged(); + if (arg instanceof ViewEvent.StatusChange) { + ViewEvent.StatusChange statChange = (ViewEvent.StatusChange) arg; + this.statusChanged(statChange.status, mServerFeatures); } else if (arg instanceof ViewEvent.PasswordSet) { this.showPasswordDialog(false); } else if (arg instanceof ViewEvent.MissingAccount) { @@ -232,8 +242,9 @@ private void updateOnEDT(Object arg) { } } - private void statusChanged() { - Control.Status status = mControl.getCurrentStatus(); + private void statusChanged(Control.Status status, EnumSet features) { + mCurrentStatus = status; + mServerFeatures = features; switch (status) { case CONNECTING: mStatusBarLabel.setText(Tr.tr("Connecting…")); From 9bc7999332629b40b7f26890a7bbc8cd68d05d65 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 16:39:41 +0100 Subject: [PATCH 118/257] view: disallow setting user avatar if not supported by server --- .../kontalk/client/AvatarSendReceiver.java | 2 ++ src/main/java/org/kontalk/client/Client.java | 12 ++++----- .../java/org/kontalk/view/ProfileDialog.java | 25 +++++++++++++------ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index 51f9efd2..141a33c8 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -107,6 +107,8 @@ void publish(String id, byte[] data) { return; } + // TODO + LOGGER.warning("not implemented"); // publish meta data... } diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 79bcd1fd..73ed932a 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -70,7 +70,7 @@ public final class Client implements StanzaListener, Runnable { private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); public enum PresenceCommand {REQUEST, GRANT, DENY}; - public enum ServerFeature {PUBSUB} + public enum ServerFeature {USER_AVATAR} private enum Command {CONNECT, DISCONNECT}; @@ -93,7 +93,7 @@ public Client(Control control) { //SmackConfiguration.DEBUG = true; mFeatureMap = new HashMap<>(); - mFeatureMap.put(PubSub.NAMESPACE, ServerFeature.PUBSUB); + mFeatureMap.put(PubSub.NAMESPACE, ServerFeature.USER_AVATAR); mFeatures = EnumSet.noneOf(ServerFeature.class); // setting caps cache @@ -464,10 +464,10 @@ public void publishAvatar(String id, byte[] data) { LOGGER.warning("no avatar sender"); return; } - if (mFeatures.contains(Client.ServerFeature.PUBSUB)) { + if (mFeatures.contains(Client.ServerFeature.USER_AVATAR)) { mAvatarSendReceiver.publish(id, data); } else { - LOGGER.info("not supported by server"); + LOGGER.warning("not supported by server"); } } @@ -477,10 +477,10 @@ public boolean deleteAvatar() { return false; } - if (mFeatures.contains(Client.ServerFeature.PUBSUB)) { + if (mFeatures.contains(Client.ServerFeature.USER_AVATAR)) { return mAvatarSendReceiver.delete(); } else { - LOGGER.info("not supported by server"); + LOGGER.warning("not supported by server"); return false; } } diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 1b41d983..62e0a3ae 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -30,6 +30,7 @@ import com.alee.laf.scroll.WebScrollPane; import com.alee.laf.separator.WebSeparator; import com.alee.laf.text.WebTextField; +import com.alee.managers.tooltip.TooltipManager; import com.alee.utils.ImageUtils; import com.alee.utils.filefilter.ImageFilesFilter; import java.awt.BorderLayout; @@ -46,8 +47,10 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.ObjectUtils; +import org.kontalk.client.Client; import org.kontalk.model.Avatar; import org.kontalk.system.Config; +import org.kontalk.system.Control; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; @@ -81,6 +84,7 @@ final class ProfileDialog extends WebDialog { groupPanel.add(new WebLabel(Tr.tr("Edit your profile")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); + // avatar mImgChooser = new WebFileChooser(); mImgChooser.setFileFilter(new ImageFilesFilter()); @@ -92,6 +96,9 @@ final class ProfileDialog extends WebDialog { //mAvatarImage.setDisplayType(DisplayType.fitComponent); //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); + + // permanent, user has to re-open the dialog on change + final boolean supported = mView.serverFeatures().contains(Client.ServerFeature.USER_AVATAR); mAvatarImage.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { @@ -102,27 +109,31 @@ public void mouseReleased(MouseEvent e) { check(e); } private void check(MouseEvent e) { - if (e.isPopupTrigger()) { + if (supported && e.isPopupTrigger()) { ProfileDialog.this.showPopupMenu(e); } } @Override public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) { + if (supported && e.getButton() == MouseEvent.BUTTON1) { ProfileDialog.this.chooseAvatar(); } } }); + mAvatarImage.setEnabled(supported); + if (!supported) + TooltipManager.addTooltip(mAvatarImage, + mView.currentStatus() != Control.Status.CONNECTED ? + Tr.tr("Not connected") : + Tr.tr("Not supported by server")); + groupPanel.add(mAvatarImage); groupPanel.add(new WebSeparator(true, true)); + // status text String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); List stats = new ArrayList<>(Arrays.asList(strings)); - String currentStatus = ""; - if (!stats.isEmpty()) - currentStatus = stats.remove(0); - - stats.remove(""); + String currentStatus = !stats.isEmpty() ? stats.remove(0) : ""; groupPanel.add(new WebLabel(Tr.tr("Your current status:"))); mStatusField = new WebTextField(currentStatus, 30); From cf601f84716ac44dff4ef2e52fb472798dfe28ea Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 16:43:26 +0100 Subject: [PATCH 119/257] (auto)update translation strings --- src/main/resources/i18n/strings.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index b952b6ed..ab3b203a 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -246,3 +246,5 @@ s_6K2I = Download profile pictures s_08GL = Download contact profile pictures s_T2TO = Show yourself in contacts s_F1X0 = Show yourself in the contact list +s_3PFR = Remove +s_D7FH = Not supported by server From 970fc415ba49a9cdb4f9b98c1f4777cc90b1844f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 16:51:30 +0100 Subject: [PATCH 120/257] control: renamed methods to reflect notification pattern --- .../kontalk/client/AcknowledgedListener.java | 2 +- .../org/kontalk/client/BlockListListener.java | 2 +- .../org/kontalk/client/BlockSendReceiver.java | 2 +- .../kontalk/client/KonMessageListener.java | 8 +-- .../org/kontalk/client/KonMessageSender.java | 2 +- .../org/kontalk/client/PublicKeyListener.java | 2 +- .../org/kontalk/client/VCardListener.java | 2 +- src/main/java/org/kontalk/system/Control.java | 61 ++++++++++--------- .../org/kontalk/system/RosterHandler.java | 2 +- 9 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/kontalk/client/AcknowledgedListener.java b/src/main/java/org/kontalk/client/AcknowledgedListener.java index 84d9ac00..b5804c2b 100644 --- a/src/main/java/org/kontalk/client/AcknowledgedListener.java +++ b/src/main/java/org/kontalk/client/AcknowledgedListener.java @@ -65,6 +65,6 @@ public void processPacket(Stanza p) { return; } - mControl.messageSent(MessageIDs.to(m)); + mControl.onMessageSent(MessageIDs.to(m)); } } diff --git a/src/main/java/org/kontalk/client/BlockListListener.java b/src/main/java/org/kontalk/client/BlockListListener.java index cd61c3ee..6197b451 100644 --- a/src/main/java/org/kontalk/client/BlockListListener.java +++ b/src/main/java/org/kontalk/client/BlockListListener.java @@ -56,7 +56,7 @@ public void processPacket(Stanza packet) { List jids = new ArrayList<>(items.size()); for (String s : items) jids.add(JID.full(s)); - mControl.setBlockedContacts(jids.toArray(new JID[0])); + mControl.onBlockList(jids.toArray(new JID[0])); } } } diff --git a/src/main/java/org/kontalk/client/BlockSendReceiver.java b/src/main/java/org/kontalk/client/BlockSendReceiver.java index f095957e..1209141d 100644 --- a/src/main/java/org/kontalk/client/BlockSendReceiver.java +++ b/src/main/java/org/kontalk/client/BlockSendReceiver.java @@ -73,6 +73,6 @@ public void processPacket(Stanza packet) return; } - mControl.setContactBlocking(mJID, mBlocking); + mControl.onContactBlocked(mJID, mBlocking); } }; diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 196dcade..67432fbb 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -107,7 +107,7 @@ public void processPacket(Stanza packet) { return; } String text = StringUtils.defaultString(error.getDescriptiveText()); - mControl.setMessageError(MessageIDs.from(m), error.getCondition(), text); + mControl.onMessageError(MessageIDs.from(m), error.getCondition(), text); return; } @@ -125,7 +125,7 @@ private void processReceiptMessage(Message m, DeliveryReceipt receipt) { if (receiptID == null || receiptID.isEmpty()) { LOGGER.warning("message has invalid receipt ID: "+receiptID); } else { - mControl.setReceived(MessageIDs.from(m, receiptID)); + mControl.onMessageReceived(MessageIDs.from(m, receiptID)); } // we ignore anything else that might be in this message } @@ -162,7 +162,7 @@ private void processChatMessage(Message m) { ChatState chatState = null; if (csExt != null) { chatState = ((ChatStateExtension) csExt).getChatState(); - mControl.processChatState(ids, + mControl.onChatStateNotification(ids, optServerDate, chatState); } @@ -183,7 +183,7 @@ private void processChatMessage(Message m) { } // add message - boolean success = mControl.newInMessage(ids, optServerDate, content); + boolean success = mControl.onNewInMessage(ids, optServerDate, content); // on success, send a 'received' for a request (XEP-0184) DeliveryReceiptRequest request = DeliveryReceiptRequest.from(m); diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 3029d460..4483b9d4 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -104,7 +104,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { !message.getCoderStatus().getErrors().isEmpty()) { LOGGER.warning("encryption failed"); message.setStatus(KonMessage.Status.ERROR); - mControl.handleSecurityErrors(message); + mControl.onSecurityErrors(message); return false; } protoMessage.addExtension(new E2EEncryption(encryptedData)); diff --git a/src/main/java/org/kontalk/client/PublicKeyListener.java b/src/main/java/org/kontalk/client/PublicKeyListener.java index 2b9a0bf9..986eb0de 100644 --- a/src/main/java/org/kontalk/client/PublicKeyListener.java +++ b/src/main/java/org/kontalk/client/PublicKeyListener.java @@ -63,7 +63,7 @@ public void processPacket(Stanza packet) { LOGGER.warning("got public key packet without public key"); return; } - mControl.handlePGPKey(JID.bare(publicKeyPacket.getFrom()), keyData); + mControl.onPGPKey(JID.bare(publicKeyPacket.getFrom()), keyData); } } diff --git a/src/main/java/org/kontalk/client/VCardListener.java b/src/main/java/org/kontalk/client/VCardListener.java index 303cf70b..35b84290 100644 --- a/src/main/java/org/kontalk/client/VCardListener.java +++ b/src/main/java/org/kontalk/client/VCardListener.java @@ -53,7 +53,7 @@ public void processPacket(Stanza packet) { LOGGER.warning("got vcard without pgp key included"); return; } - mControl.handlePGPKey(JID.bare(p.getFrom()), publicKey); + mControl.onPGPKey(JID.bare(p.getFrom()), publicKey); } } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 569f68f4..c30f75f5 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -144,7 +144,7 @@ public void onException(KonException ex) { } // TODO unused - public void handleEncryptionErrors(KonMessage message, Contact contact) { + public void onEncryptionErrors(KonMessage message, Contact contact) { EnumSet errors = message.getCoderStatus().getErrors(); if (errors.contains(Coder.Error.KEY_UNAVAILABLE) || errors.contains(Coder.Error.INVALID_SIGNATURE) || @@ -152,10 +152,10 @@ public void handleEncryptionErrors(KonMessage message, Contact contact) { // maybe there is something wrong with the senders key this.maySendKeyRequest(contact); } - this.handleSecurityErrors(message); + this.onSecurityErrors(message); } - public void handleSecurityErrors(KonMessage message) { + public void onSecurityErrors(KonMessage message) { mViewControl.changed(new ViewEvent.SecurityError(message)); } @@ -164,7 +164,7 @@ public void handleSecurityErrors(KonMessage message) { * receipts): Create, save and process the message. * @return true on success or message is a duplicate, false on unexpected failure */ - public boolean newInMessage(MessageIDs ids, + public boolean onNewInMessage(MessageIDs ids, Optional serverDate, MessageContent content) { LOGGER.info("new incoming message, "+ids); @@ -225,7 +225,7 @@ public boolean newInMessage(MessageIDs ids, return newMessage.getID() >= -1; } - public void messageSent(MessageIDs ids) { + public void onMessageSent(MessageIDs ids) { OutMessage message = findMessage(ids).orElse(null); if (message == null) return; @@ -233,7 +233,7 @@ public void messageSent(MessageIDs ids) { message.setStatus(KonMessage.Status.SENT); } - public void setReceived(MessageIDs ids) { + public void onMessageReceived(MessageIDs ids) { OutMessage message = findMessage(ids).orElse(null); if (message == null) return; @@ -241,7 +241,7 @@ public void setReceived(MessageIDs ids) { message.setReceived(ids.jid); } - public void setMessageError(MessageIDs ids, Condition condition, String errorText) { + public void onMessageError(MessageIDs ids, Condition condition, String errorText) { OutMessage message = findMessage(ids).orElse(null); if (message == null) return ; @@ -251,7 +251,7 @@ public void setMessageError(MessageIDs ids, Condition condition, String errorTex /** * Inform model (and view) about a received chat state notification. */ - public void processChatState(MessageIDs ids, + public void onChatStateNotification(MessageIDs ids, Optional serverDate, ChatState chatState) { if (serverDate.isPresent()) { @@ -274,17 +274,17 @@ public void processChatState(MessageIDs ids, chat.setChatState(contact, chatState); } - public void handlePGPKey(JID jid, byte[] rawKey) { + public void onPGPKey(JID jid, byte[] rawKey) { Contact contact = ContactList.getInstance().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid: "+jid); return; } - this.handlePGPKey(contact, rawKey); + this.onPGPKey(contact, rawKey); } - void handlePGPKey(Contact contact, byte[] rawKey) { + void onPGPKey(Contact contact, byte[] rawKey) { PGPCoderKey key = PGPUtils.readPublicKey(rawKey).orElse(null); if (key == null) { LOGGER.warning("invalid public PGP key, contact: "+contact); @@ -308,32 +308,17 @@ void handlePGPKey(Contact contact, byte[] rawKey) { } } - public void setKey(Contact contact, PGPCoderKey key) { - contact.setKey(key.rawKey, key.fingerprint); - - // enable encryption without asking - contact.setEncrypted(true); - - // if not set, use uid in key for contact name - if (contact.getName().isEmpty() && key.userID != null) { - LOGGER.info("full UID in key: '" + key.userID + "'"); - String contactName = PGPUtils.parseUID(key.userID)[0]; - if (!contactName.isEmpty()) - contact.setName(contactName); - } - } - - public void setBlockedContacts(JID[] jids) { + public void onBlockList(JID[] jids) { for (JID jid : jids) { if (jid.isFull()) { LOGGER.info("ignoring blocking of JID with resource"); return; } - this.setContactBlocking(jid, true); + this.onContactBlocked(jid, true); } } - public void setContactBlocking(JID jid, boolean blocking) { + public void onContactBlocked(JID jid, boolean blocking) { Contact contact = ContactList.getInstance().get(jid).orElse(null); if (contact == null) { LOGGER.info("ignoring blocking of JID not in contact list"); @@ -457,12 +442,28 @@ private void decryptAndProcess(InMessage message) { this.processContent(message); } + + private void setKey(Contact contact, PGPCoderKey key) { + contact.setKey(key.rawKey, key.fingerprint); + + // enable encryption without asking + contact.setEncrypted(true); + + // if not set, use uid in key for contact name + if (contact.getName().isEmpty() && key.userID != null) { + LOGGER.info("full UID in key: '" + key.userID + "'"); + String contactName = PGPUtils.parseUID(key.userID)[0]; + if (!contactName.isEmpty()) + contact.setName(contactName); + } + } + /** * Download attachment for incoming message if present. */ private void processContent(InMessage message) { if (!message.getCoderStatus().getErrors().isEmpty()) { - this.handleSecurityErrors(message); + this.onSecurityErrors(message); } if (message.getContent().getPreview().isPresent()) { diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 86349f68..c2667fa0 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -127,7 +127,7 @@ public void onSubscriptionRequest(JID jid, byte[] rawKey) { } if (rawKey.length > 0) - mControl.handlePGPKey(contact, rawKey); + mControl.onPGPKey(contact, rawKey); } public void onPresenceUpdate(JID jid, Presence.Type type, String status) { From 04143b029265661eea3d7ef62c72b5dc7588632d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 1 Feb 2016 22:39:20 +0100 Subject: [PATCH 121/257] cleanup of threading - all threads got a name - all threads became daemons - additional main thread doing nothing forever --- src/main/java/org/kontalk/Kontalk.java | 38 ++++++++++++++++--- src/main/java/org/kontalk/client/Client.java | 12 +++++- .../kontalk/client/PrivateKeyReceiver.java | 6 ++- .../org/kontalk/system/AttachmentManager.java | 4 +- .../org/kontalk/system/ChatStateManager.java | 2 +- src/main/java/org/kontalk/system/Control.java | 4 +- src/main/java/org/kontalk/util/OggClip.java | 15 ++++---- src/main/java/org/kontalk/view/LinkUtils.java | 2 +- 8 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 0e195827..e6e180d7 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -61,6 +61,8 @@ public final class Kontalk { private static ServerSocket RUN_LOCK = null; private static Path APP_DIR = null; + ViewControl mControl = null; + Kontalk() { // platform dependent configuration directory this(Paths.get(System.getProperty("user.home"), @@ -132,26 +134,26 @@ void start(boolean ui) { // register provider PGPUtils.registerProvider(); - final ViewControl control = Control.create(); + mControl = Control.create(); // handle shutdown signals - Runtime.getRuntime().addShutdownHook(new Thread() { + Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { @Override public void run() { // NOTE: logging does not work here anymore already - control.shutDown(false); + mControl.shutDown(false); System.out.println("shutdown finished"); } }); - View view = ui ? View.create(control).orElse(null) : null; + View view = ui ? View.create(mControl).orElse(null) : null; try { // do now to test if successful Database.initialize(); } catch (KonException ex) { LOGGER.log(Level.SEVERE, "can't initialize database", ex); - control.shutDown(true); + mControl.shutDown(true); return; // never reached } @@ -162,7 +164,31 @@ public void run() { if (view != null) view.init(); - control.launch(); + mControl.launch(); + + new Thread("Kontalk Main") { + @Override + public void run() { + try { + // wait until exit call + Object lock = new Object(); + synchronized (lock) { + lock.wait(); + } + } catch (InterruptedException ex) { + LOGGER.log(Level.WARNING, "interrupted while waiting", ex); + } + } + }.start(); + } + + void stop() { + if (mControl == null) { + LOGGER.warning("not started"); + return; + } + + mControl.shutDown(true); } public static Path appDir() { diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 73ed932a..3a926600 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -83,7 +83,7 @@ private enum Command {CONNECT, DISCONNECT}; private KonConnection mConn = null; private AvatarSendReceiver mAvatarSendReceiver = null; - public Client(Control control) { + private Client(Control control) { mControl = control; //mLimited = limited; @@ -110,6 +110,16 @@ public Client(Control control) { new SimpleDirectoryPersistentCache(cacheDir)); } + public static Client create(Control control) { + Client client = new Client(control); + + Thread clientThread = new Thread(client, "Client Connector"); + clientThread.setDaemon(true); + clientThread.start(); + + return client; + } + public void connect(PersonalKey key) { this.disconnect(); diff --git a/src/main/java/org/kontalk/client/PrivateKeyReceiver.java b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java index bbb4c822..2d436532 100644 --- a/src/main/java/org/kontalk/client/PrivateKeyReceiver.java +++ b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java @@ -58,12 +58,14 @@ public void sendRequest(EndpointServer server, boolean validateCertificate, // create connection mConn = new KonConnection(server, validateCertificate); - new Thread() { + Thread thread = new Thread("Private Key Request") { @Override public void run() { PrivateKeyReceiver.this.sendRequestAsync(registrationToken); } - }.start(); + }; + thread.setDaemon(true); + thread.start(); } private void sendRequestAsync(String registrationToken) { diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 00ac5053..c25a7e26 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -340,7 +340,9 @@ public static boolean isImage(String mimeType) { static AttachmentManager create(Control control) { AttachmentManager manager = new AttachmentManager(Kontalk.appDir(), control); - new Thread(manager).start(); + Thread thread = new Thread(manager, "Attachment Transfer"); + thread.setDaemon(true); + thread.start(); return manager; } diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index 468435ab..747ef6a6 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -39,7 +39,7 @@ final class ChatStateManager { private final Client mClient; private final Map mChatStateCache = new HashMap<>(); - private final Timer mTimer = new Timer(); + private final Timer mTimer = new Timer("Chat State Timer", true); public ChatStateManager(Client client) { mClient = client; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index c30f75f5..d9c2bcd4 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -94,7 +94,7 @@ public enum Status { private Control() { mViewControl = new ViewControl(); - mClient = new Client(this); + mClient = Client.create(this); mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(this); mRosterHandler = new RosterHandler(this, mClient); @@ -538,8 +538,6 @@ private static Optional findMessage(MessageIDs ids) { public class ViewControl extends Observable { public void launch() { - new Thread(mClient).start(); - boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); if (!Account.getInstance().isPresent()) { LOGGER.info("no account found, asking for import..."); diff --git a/src/main/java/org/kontalk/util/OggClip.java b/src/main/java/org/kontalk/util/OggClip.java index 0ab90e56..fcc4de5d 100644 --- a/src/main/java/org/kontalk/util/OggClip.java +++ b/src/main/java/org/kontalk/util/OggClip.java @@ -238,7 +238,8 @@ public void play() { // ignore if no mark } - player = new Thread() { + player = new Thread("OGG Play") { + @Override public void run() { try { playStream(Thread.currentThread()); @@ -251,10 +252,9 @@ public void run() { } catch (IOException e) { e.printStackTrace(); } - } - ; + }; }; - player.setDaemon(true); + player.setDaemon(true); player.start(); } @@ -270,7 +270,7 @@ public void loop() { // ignore if no mark } - player = new Thread() { + player = new Thread("OGG Loop") { @Override public void run() { while (player == Thread.currentThread()) { @@ -286,10 +286,9 @@ public void run() { } catch (IOException e) { } } - } - ; + }; }; - player.setDaemon(true); + player.setDaemon(true); player.start(); } diff --git a/src/main/java/org/kontalk/view/LinkUtils.java b/src/main/java/org/kontalk/view/LinkUtils.java index de4891cf..e37c1f1c 100644 --- a/src/main/java/org/kontalk/view/LinkUtils.java +++ b/src/main/java/org/kontalk/view/LinkUtils.java @@ -118,7 +118,7 @@ public void run() { WebUtils.browseSiteSafely(fixProto(url)); } }; - new Thread(run).start(); + new Thread(run, "Link Browser").start(); } } From c7f3807e04a9761f21ddc0bcc3224335350d3ed2 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 2 Feb 2016 18:10:57 +0100 Subject: [PATCH 122/257] model: fix getting user contact --- src/main/java/org/kontalk/model/ContactList.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 3de3e12a..c648ebc6 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -120,9 +120,9 @@ public Optional get(JID jid) { */ public Optional getMe() { JID myJID = Account.getInstance().getUserJID(); - Contact contact = this.get(myJID).orElse(null); - if (contact == null) - return Optional.empty(); + Contact me = this.get(myJID).orElse(null); + if (me != null) + return Optional.of(me); return this.create(myJID, ""); } From ab4f8c451ca4bae291280d370e2e7d83329b8739 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 2 Feb 2016 18:17:30 +0100 Subject: [PATCH 123/257] model: member role added to database; database changed --- src/main/java/org/kontalk/model/Chat.java | 171 +++++++++++------- .../java/org/kontalk/model/GroupChat.java | 5 + .../java/org/kontalk/model/SingleChat.java | 5 + .../java/org/kontalk/system/Database.java | 20 +- 4 files changed, 128 insertions(+), 73 deletions(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index f2cba3cb..4a98e091 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -23,6 +23,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -37,7 +38,6 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.json.simple.JSONObject; import org.json.simple.JSONValue; -import org.kontalk.model.GroupMetaData; import org.kontalk.system.Database; /** @@ -74,16 +74,22 @@ public abstract class Chat extends Observable implements Observer { public static final String RECEIVER_TABLE = "receiver"; public static final String COL_REC_CHAT_ID = "thread_id"; public static final String COL_REC_CONTACT_ID = "user_id"; + public static final String COL_REC_ROLE = "role"; public static final String RECEIVER_SCHEMA = "(" + Database.SQL_ID + COL_REC_CHAT_ID+" INTEGER NOT NULL, " + COL_REC_CONTACT_ID+" INTEGER NOT NULL, " + + COL_REC_ROLE+" INTEGER NOT NULL, " + "UNIQUE ("+COL_REC_CHAT_ID+", "+COL_REC_CONTACT_ID+"), " + "FOREIGN KEY ("+COL_REC_CHAT_ID+") REFERENCES "+TABLE+" (_id), " + "FOREIGN KEY ("+COL_REC_CONTACT_ID+") REFERENCES "+Contact.TABLE+" (_id) " + ")"; - /** Long-live authorization model of member in group. 'Affiliation' in MUC */ + /** + * Long-live authorization model of member in group. + * Called 'Affiliation' in MUC + * Do not modify, only add! Ordinal used in database + */ public enum Role {DEFAULT, OWNER, ADMIN}; protected final int mID; @@ -117,7 +123,7 @@ protected Chat(List members, String xmppID, String subject, GroupMetaDat } for (Member member : members) - this.insertMember(member); + member.insert(mID); } // used when loading from database @@ -181,7 +187,11 @@ public boolean isGroupChat() { return (this instanceof GroupChat); } - /** Get all contacts (including deleted, blocked and user contact). */ + protected abstract List getAllMembers(); + + /** Get all contacts (including deleted, blocked and user contact). + * TODO remove me + */ public abstract List getAllContacts(); /** Get valid receiver contacts (without deleted and blocked). */ @@ -224,19 +234,19 @@ protected void save(List members, String subject) { db.execUpdate(TABLE, set, mID); // get receiver for this chat - Map dbReceiver = loadReceiver(mID); + List oldMembers = this.getAllMembers(); - // add missing contact - for (Member member : members) { - if (!dbReceiver.keySet().contains(member.contact.getID())) { - this.insertMember(member); + // save new members + for (Member m : members) { + if (!oldMembers.contains(m)) { + m.insert(mID); } - dbReceiver.remove(member.contact.getID()); + oldMembers.remove(m); } - // whats left is too much and can be removed - for (int id : dbReceiver.values()) { - db.execDelete(RECEIVER_TABLE, id); + // whats left is too much and can be deleted + for (Member m : oldMembers) { + m.delete(); } } @@ -246,35 +256,29 @@ void delete() { String whereMessages = KonMessage.COL_CHAT_ID + " == " + mID; // transmissions - db.execDeleteWhereInsecure(Transmission.TABLE, + boolean succ = db.execDeleteWhereInsecure(Transmission.TABLE, Transmission.COL_MESSAGE_ID + " IN (SELECT _id FROM " + KonMessage.TABLE + " WHERE " + whereMessages + ")"); + if (!succ) + return; // messages - db.execDeleteWhereInsecure(KonMessage.TABLE, whereMessages); + succ = db.execDeleteWhereInsecure(KonMessage.TABLE, whereMessages); + if (!succ) + return; - // receiver - Map dbReceiver = loadReceiver(mID); - for (int id : dbReceiver.values()) { - boolean deleted = db.execDelete(RECEIVER_TABLE, id); - if (!deleted) return; + // members + boolean allDeleted = true; + for (Member member : this.getAllMembers()) { + allDeleted &= member.delete(); } + if (!allDeleted) + return; // chat itself db.execDelete(TABLE, mID); } - private void insertMember(Member member) { - Database db = Database.getInstance(); - List recValues = new LinkedList<>(); - recValues.add(mID); - recValues.add(member.contact.getID()); - int id = db.execInsert(RECEIVER_TABLE, recValues); - if (id < 1) { - LOGGER.warning("could not insert receiver"); - } - } - protected void changed(Object arg) { this.setChanged(); this.notifyObservers(arg); @@ -295,16 +299,8 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String xmppID = Database.getString(rs, Chat.COL_XMPPID); - // get members for chats - Map dbReceiver = Chat.loadReceiver(id); - List members = new ArrayList<>(); - for (int conID: dbReceiver.keySet()) { - Contact c = ContactList.getInstance().get(conID).orElse(null); - if (c != null) - members.add(new Member(c)); - else - LOGGER.warning("can't find contact, ID:"+conID); - } + // get members for chat + List members = Member.load(id); String subject = Database.getString(rs, Chat.COL_SUBJ); @@ -324,33 +320,12 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { } } - static Map loadReceiver(int chatID) { - Database db = Database.getInstance(); - String where = COL_REC_CHAT_ID + " == " + chatID; - Map dbReceiver = new HashMap<>(); - ResultSet resultSet; - try { - resultSet = db.execSelectWhereInsecure(RECEIVER_TABLE, where); - } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "can't get receiver from db", ex); - return dbReceiver; - } - try { - while (resultSet.next()) { - dbReceiver.put(resultSet.getInt(COL_REC_CONTACT_ID), - resultSet.getInt("_id")); - } - resultSet.close(); - } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "can't get receiver", ex); - } - return dbReceiver; - } - public static final class Member { public final Contact contact; public final GroupChat.Role role; + private int id; + private ChatState mState = ChatState.gone; // note: the Android client does not set active states when only viewing // the chat (not necessary according to XEP-0085), this makes the @@ -363,6 +338,11 @@ public Member(Contact contact){ } public Member(Contact contact, GroupChat.Role role) { + this(0, contact, role); + } + + private Member(int id, Contact contact, GroupChat.Role role) { + this.id = id; this.contact = contact; this.role = role; } @@ -394,11 +374,74 @@ public ChatState getState() { return mState; } + private boolean insert(int chatID) { + if (id > 0) { + LOGGER.warning("already in database"); + return true; + } + + List recValues = new LinkedList<>(); + recValues.add(chatID); + recValues.add(contact.getID()); + recValues.add(role); + id = Database.getInstance().execInsert(RECEIVER_TABLE, recValues); + if (id <= 0) { + LOGGER.warning("could not insert member"); + return false; + } + return true; + } + + private void save() { + // TODO + } + + private boolean delete() { + if (id <= 0) { + LOGGER.warning("not in database"); + return true; + } + + return Database.getInstance().execDelete(RECEIVER_TABLE, id); + } + protected void setState(ChatState state) { mState = state; if (mState == ChatState.active || mState == ChatState.composing) mLastActive = new Date(); } + + static List load(int chatID) { + Database db = Database.getInstance(); + String where = COL_REC_CHAT_ID + " == " + chatID; + ResultSet resultSet; + try { + resultSet = db.execSelectWhereInsecure(RECEIVER_TABLE, where); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't get receiver from db", ex); + return Collections.emptyList(); + } + List members = new ArrayList<>(); + try { + while (resultSet.next()) { + int id = resultSet.getInt("_id"); + int contactID = resultSet.getInt(COL_REC_CONTACT_ID); + int r = resultSet.getInt(COL_REC_ROLE); + Role role = Role.values()[r]; + Contact c = ContactList.getInstance().get(contactID).orElse(null); + if (c == null) { + LOGGER.warning("can't find contact, ID:"+contactID); + continue; + } + + members.add(new Member(id, c, role)); + } + resultSet.close(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't get members", ex); + } + return members; + } } public static class ViewSettings { diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index e5d8f8d1..72118348 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -73,6 +73,11 @@ private GroupChat(int id, this.addMemberSilent(member); } + @Override + protected List getAllMembers() { + return new ArrayList<>(mMemberSet); + } + /** Get all contacts (including deleted and user contact). */ @Override public List getAllContacts() { diff --git a/src/main/java/org/kontalk/model/SingleChat.java b/src/main/java/org/kontalk/model/SingleChat.java index 34ae426d..3f13c390 100644 --- a/src/main/java/org/kontalk/model/SingleChat.java +++ b/src/main/java/org/kontalk/model/SingleChat.java @@ -61,6 +61,11 @@ public Contact getContact() { return mMember.contact; } + @Override + protected List getAllMembers() { + return Arrays.asList(mMember); + } + @Override public List getAllContacts() { return Arrays.asList(mMember.contact); diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 9cae548d..dee6a0fd 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -63,7 +63,7 @@ public final class Database { public static final String SQL_ID = "_id INTEGER PRIMARY KEY AUTOINCREMENT, "; private static final String FILENAME = "kontalk_db.sqlite"; - private static final int DB_VERSION = 4; + private static final int DB_VERSION = 5; private static final String SQL_CREATE = "CREATE TABLE IF NOT EXISTS "; private static final String SV = "schema_version"; private static final String UV = "user_version"; @@ -132,10 +132,12 @@ private Database(Path path) throws KonException { return; } LOGGER.config("version: "+version); - try { - this.update(version); - } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "can't update db", ex); + if (version < DB_VERSION) { + try { + this.update(version); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't update db", ex); + } } } @@ -144,9 +146,6 @@ private void createTable(Statement stat, String table, String schema) throws SQL } private void update(int fromVersion) throws SQLException { - if (fromVersion >= DB_VERSION) - return; - if (fromVersion < 1) { mConn.createStatement().execute("ALTER TABLE "+Chat.TABLE+ " ADD COLUMN "+Chat.COL_VIEW_SET+" NOT NULL DEFAULT '{}'"); @@ -180,7 +179,10 @@ private void update(int fromVersion) throws SQLException { mConn.createStatement().execute("ALTER TABLE "+Contact.TABLE+ " ADD COLUMN "+Contact.COL_AVATAR_ID+" DEFAULT NULL"); } - + if (fromVersion < 5) { + mConn.createStatement().execute("ALTER TABLE "+Chat.RECEIVER_TABLE+ + " ADD COLUMN "+Chat.COL_REC_ROLE+" DEFAULT 0"); + } // set new version mConn.createStatement().execute("PRAGMA "+UV+" = "+DB_VERSION); From 6f66dca4d2fbaacda1187c902bdf0fdb3ec9483b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 2 Feb 2016 19:01:39 +0100 Subject: [PATCH 124/257] model: member class moved to new file --- src/main/java/org/kontalk/model/Chat.java | 151 +------------- .../java/org/kontalk/model/GroupChat.java | 13 +- src/main/java/org/kontalk/model/Member.java | 195 ++++++++++++++++++ .../java/org/kontalk/model/SingleChat.java | 15 +- .../java/org/kontalk/system/Database.java | 7 +- .../java/org/kontalk/view/ChatListView.java | 7 +- .../java/org/kontalk/view/MessageList.java | 3 +- 7 files changed, 221 insertions(+), 170 deletions(-) create mode 100644 src/main/java/org/kontalk/model/Member.java diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 4a98e091..17fcbd67 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -21,10 +21,7 @@ import java.awt.Color; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -70,28 +67,6 @@ public abstract class Chat extends Observable implements Observer { COL_GD+" TEXT " + ")"; - // many to many relationship requires additional table for receiver - public static final String RECEIVER_TABLE = "receiver"; - public static final String COL_REC_CHAT_ID = "thread_id"; - public static final String COL_REC_CONTACT_ID = "user_id"; - public static final String COL_REC_ROLE = "role"; - public static final String RECEIVER_SCHEMA = "(" + - Database.SQL_ID + - COL_REC_CHAT_ID+" INTEGER NOT NULL, " + - COL_REC_CONTACT_ID+" INTEGER NOT NULL, " + - COL_REC_ROLE+" INTEGER NOT NULL, " + - "UNIQUE ("+COL_REC_CHAT_ID+", "+COL_REC_CONTACT_ID+"), " + - "FOREIGN KEY ("+COL_REC_CHAT_ID+") REFERENCES "+TABLE+" (_id), " + - "FOREIGN KEY ("+COL_REC_CONTACT_ID+") REFERENCES "+Contact.TABLE+" (_id) " + - ")"; - - /** - * Long-live authorization model of member in group. - * Called 'Affiliation' in MUC - * Do not modify, only add! Ordinal used in database - */ - public enum Role {DEFAULT, OWNER, ADMIN}; - protected final int mID; private final ChatMessages mMessages; @@ -316,131 +291,7 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { LOGGER.warning("not one contact for single chat, id="+id); return null; } - return new SingleChat(id, members.get(0).contact, xmppID, read, jsonViewSettings); - } - } - - public static final class Member { - public final Contact contact; - public final GroupChat.Role role; - - private int id; - - private ChatState mState = ChatState.gone; - // note: the Android client does not set active states when only viewing - // the chat (not necessary according to XEP-0085), this makes the - // extra date field a bit useless - // TODO save last active date to DB - private Date mLastActive = null; - - public Member(Contact contact){ - this(contact, Role.DEFAULT); - } - - public Member(Contact contact, GroupChat.Role role) { - this(0, contact, role); - } - - private Member(int id, Contact contact, GroupChat.Role role) { - this.id = id; - this.contact = contact; - this.role = role; - } - - @Override - public boolean equals(Object o) { - if (o == this) - return true; - - if (!(o instanceof Member)) - return false; - - return this.contact.equals(o); - } - - @Override - public int hashCode() { - int hash = 7; - hash = 23 * hash + Objects.hashCode(this.contact); - return hash; - } - - @Override - public String toString() { - return "Mem:c={"+contact+"}r="+role; - } - - public ChatState getState() { - return mState; - } - - private boolean insert(int chatID) { - if (id > 0) { - LOGGER.warning("already in database"); - return true; - } - - List recValues = new LinkedList<>(); - recValues.add(chatID); - recValues.add(contact.getID()); - recValues.add(role); - id = Database.getInstance().execInsert(RECEIVER_TABLE, recValues); - if (id <= 0) { - LOGGER.warning("could not insert member"); - return false; - } - return true; - } - - private void save() { - // TODO - } - - private boolean delete() { - if (id <= 0) { - LOGGER.warning("not in database"); - return true; - } - - return Database.getInstance().execDelete(RECEIVER_TABLE, id); - } - - protected void setState(ChatState state) { - mState = state; - if (mState == ChatState.active || mState == ChatState.composing) - mLastActive = new Date(); - } - - static List load(int chatID) { - Database db = Database.getInstance(); - String where = COL_REC_CHAT_ID + " == " + chatID; - ResultSet resultSet; - try { - resultSet = db.execSelectWhereInsecure(RECEIVER_TABLE, where); - } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "can't get receiver from db", ex); - return Collections.emptyList(); - } - List members = new ArrayList<>(); - try { - while (resultSet.next()) { - int id = resultSet.getInt("_id"); - int contactID = resultSet.getInt(COL_REC_CONTACT_ID); - int r = resultSet.getInt(COL_REC_ROLE); - Role role = Role.values()[r]; - Contact c = ContactList.getInstance().get(contactID).orElse(null); - if (c == null) { - LOGGER.warning("can't find contact, ID:"+contactID); - continue; - } - - members.add(new Member(id, c, role)); - } - resultSet.close(); - } catch (SQLException ex) { - LOGGER.log(Level.WARNING, "can't get members", ex); - } - return members; + return new SingleChat(id, members.get(0).getContact(), xmppID, read, jsonViewSettings); } } diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 72118348..3f3022da 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -83,7 +83,7 @@ protected List getAllMembers() { public List getAllContacts() { List l = new ArrayList<>(mMemberSet.size()); for (Member m : mMemberSet) - l.add(m.contact); + l.add(m.getContact()); return l; } @@ -93,8 +93,9 @@ public Contact[] getValidContacts() { //chat.getContacts().stream().filter(c -> !c.isDeleted()); Set contacts = new HashSet<>(); for (Member m : mMemberSet) { - if (!m.contact.isDeleted() && !m.contact.isMe()) { - contacts.add(m.contact); + Contact c = m.getContact(); + if (!c.isDeleted() && !c.isMe()) { + contacts.add(m.getContact()); } } return contacts.toArray(new Contact[0]); @@ -131,7 +132,7 @@ private void addMemberSilent(Member member) { return; } - member.contact.addObserver(this); + member.getContact().addObserver(this); mMemberSet.add(member); } @@ -168,7 +169,7 @@ public void setChatState(final Contact contact, ChatState chatState) { Member member = mMemberSet.stream().filter(new Predicate(){ @Override public boolean test(Member t) { - return t.contact.equals(contact); + return t.getContact().equals(contact); } }).findFirst().orElse(null); @@ -281,7 +282,7 @@ private boolean containsMe() { new Predicate() { @Override public boolean test(Member t) { - return t.contact.isMe(); + return t.getContact().isMe(); } } ); diff --git a/src/main/java/org/kontalk/model/Member.java b/src/main/java/org/kontalk/model/Member.java new file mode 100644 index 00000000..e36b4059 --- /dev/null +++ b/src/main/java/org/kontalk/model/Member.java @@ -0,0 +1,195 @@ +/* + * Kontalk Java client + * Copyright (C) 2016 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.model; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jivesoftware.smackx.chatstates.ChatState; +import org.kontalk.system.Database; + +/** + * A contact association with a chat. + * Single chats have exactly one, group chats can have any number of members. + * + * @author Alexander Bikadorov {@literal } + */ +public final class Member { + private static final Logger LOGGER = Logger.getLogger(Member.class.getName()); + + /** + * Long-live authorization model of member in group. + * Called 'Affiliation' in MUC + * Do not modify, only add! Ordinal used in database + */ + public enum Role {DEFAULT, OWNER, ADMIN}; + + // many to many relationship requires additional table for members + public static final String TABLE = "receiver"; + public static final String COL_CONTACT_ID = "user_id"; + public static final String COL_ROLE = "role"; + public static final String COL_CHAT_ID = "thread_id"; + public static final String SCHEMA = "(" + + Database.SQL_ID + + COL_CHAT_ID + " INTEGER NOT NULL, " + + COL_CONTACT_ID + " INTEGER NOT NULL, " + + COL_ROLE + " INTEGER NOT NULL, " + + "UNIQUE (" + COL_CHAT_ID + ", " + COL_CONTACT_ID + "), " + + "FOREIGN KEY (" + COL_CHAT_ID + ") REFERENCES " + Chat.TABLE + " (_id), " + + "FOREIGN KEY (" + COL_CONTACT_ID + ") REFERENCES " + Contact.TABLE + " (_id) " + + ")"; + + private final Contact mContact; + private final Role mRole; + + private int id; + + private ChatState mState = ChatState.gone; + // note: the Android client does not set active states when only viewing + // the chat (not necessary according to XEP-0085), this makes the + // extra date field a bit useless + // TODO save last active date to DB + private Date mLastActive = null; + + public Member(Contact contact){ + this(contact, Role.DEFAULT); + } + + public Member(Contact contact, Role role) { + this(0, contact, role); + } + + private Member(int id, Contact contact, Role role) { + this.id = id; + this.mContact = contact; + this.mRole = role; + } + + public Contact getContact() { + return mContact; + } + + public Role getRole() { + return mRole; + } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Member)) + return false; + + // TODO dangerous + return mContact.equals(o); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 23 * hash + Objects.hashCode(mContact); + return hash; + } + + @Override + public String toString() { + return "Mem:c={"+mContact+"}r="+mRole; + } + + public ChatState getState() { + return mState; + } + + boolean insert(int chatID) { + if (id > 0) { + LOGGER.warning("already in database"); + return true; + } + + List recValues = new LinkedList<>(); + recValues.add(chatID); + recValues.add(getContact().getID()); + recValues.add(mRole); + id = Database.getInstance().execInsert(TABLE, recValues); + if (id <= 0) { + LOGGER.warning("could not insert member"); + return false; + } + return true; + } + + void save() { + // TODO + } + + boolean delete() { + if (id <= 0) { + LOGGER.warning("not in database"); + return true; + } + + return Database.getInstance().execDelete(TABLE, id); + } + + protected void setState(ChatState state) { + mState = state; + if (mState == ChatState.active || mState == ChatState.composing) + mLastActive = new Date(); + } + + static List load(int chatID) { + Database db = Database.getInstance(); + String where = COL_CHAT_ID + " == " + chatID; + ResultSet resultSet; + try { + resultSet = db.execSelectWhereInsecure(TABLE, where); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't get receiver from db", ex); + return Collections.emptyList(); + } + List members = new ArrayList<>(); + try { + while (resultSet.next()) { + int id = resultSet.getInt("_id"); + int contactID = resultSet.getInt(COL_CONTACT_ID); + int r = resultSet.getInt(COL_ROLE); + Role role = Role.values()[r]; + Contact c = ContactList.getInstance().get(contactID).orElse(null); + if (c == null) { + LOGGER.warning("can't find contact, ID:"+contactID); + continue; + } + + members.add(new Member(id, c, role)); + } + resultSet.close(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't get members", ex); + } + return members; + } +} \ No newline at end of file diff --git a/src/main/java/org/kontalk/model/SingleChat.java b/src/main/java/org/kontalk/model/SingleChat.java index 3f13c390..fde78e0d 100644 --- a/src/main/java/org/kontalk/model/SingleChat.java +++ b/src/main/java/org/kontalk/model/SingleChat.java @@ -58,7 +58,7 @@ public final class SingleChat extends Chat { } public Contact getContact() { - return mMember.contact; + return mMember.getContact(); } @Override @@ -68,12 +68,12 @@ protected List getAllMembers() { @Override public List getAllContacts() { - return Arrays.asList(mMember.contact); + return Arrays.asList(mMember.getContact()); } @Override public Contact[] getValidContacts() { - Contact c = mMember.contact; + Contact c = mMember.getContact(); if (c.isDeleted() || c.isBlocked() && !c.isMe()) return new Contact[0]; @@ -92,18 +92,19 @@ public String getSubject() { @Override public boolean isSendEncrypted() { - return mMember.contact.getEncrypted(); + return mMember.getContact().getEncrypted(); } @Override public boolean canSendEncrypted() { - Contact c = mMember.contact; + Contact c = mMember.getContact(); return !c.isDeleted() && !c.isBlocked() && c.hasKey(); } @Override public boolean isValid() { - return !mMember.contact.isDeleted() && !mMember.contact.isBlocked(); + Contact c = mMember.getContact(); + return !c.isDeleted() && !c.isBlocked(); } @Override @@ -113,7 +114,7 @@ public boolean isAdministratable() { @Override public void setChatState(Contact contact, ChatState chatState) { - if (!contact.equals(mMember.contact)) { + if (!contact.equals(mMember.getContact())) { LOGGER.warning("wrong contact!?"); return; } diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index dee6a0fd..15bf8dd7 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -41,6 +41,7 @@ import org.kontalk.model.KonMessage; import org.kontalk.model.Chat; import org.kontalk.model.Contact; +import org.kontalk.model.Member; import org.kontalk.model.Transmission; import org.kontalk.util.EncodingUtils; import org.sqlite.SQLiteConfig; @@ -113,7 +114,7 @@ private Database(Path path) throws KonException { mConn.createStatement().execute("PRAGMA "+UV+" = "+DB_VERSION); this.createTable(stat, Contact.TABLE, Contact.SCHEMA); this.createTable(stat, Chat.TABLE, Chat.SCHEMA); - this.createTable(stat, Chat.RECEIVER_TABLE, Chat.RECEIVER_SCHEMA); + this.createTable(stat, Member.TABLE, Member.SCHEMA); this.createTable(stat, KonMessage.TABLE, KonMessage.SCHEMA); this.createTable(stat, Transmission.TABLE, Transmission.SCHEMA); } catch (SQLException ex) { @@ -180,8 +181,8 @@ private void update(int fromVersion) throws SQLException { " ADD COLUMN "+Contact.COL_AVATAR_ID+" DEFAULT NULL"); } if (fromVersion < 5) { - mConn.createStatement().execute("ALTER TABLE "+Chat.RECEIVER_TABLE+ - " ADD COLUMN "+Chat.COL_REC_ROLE+" DEFAULT 0"); + mConn.createStatement().execute("ALTER TABLE "+Member.TABLE+ + " ADD COLUMN "+Member.COL_ROLE+" DEFAULT 0"); } // set new version diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 5947a4d0..6cd8e423 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -42,6 +42,7 @@ import org.kontalk.model.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.GroupChat; +import org.kontalk.model.Member; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.SingleChat; import org.kontalk.util.Tr; @@ -258,15 +259,15 @@ private void updateView(Object arg) { } String stateText = ""; - if (arg instanceof Chat.Member) { - Chat.Member member = (Chat.Member) arg; + if (arg instanceof Member) { + Member member = (Member) arg; switch(member.getState()) { case composing: stateText = Tr.tr("is writing…"); break; //case paused: activity = T/r.tr("stopped typing"); break; //case inactive: stateText = T/r.tr("is inactive"); break; } if (!stateText.isEmpty() && mValue.isGroupChat()) - stateText = member.contact.getName() + ": " + stateText; + stateText = member.getContact().getName() + ": " + stateText; } if (stateText.isEmpty()) { mChatStateLabel.setText(""); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 179159ad..3f6e1350 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -63,6 +63,7 @@ import org.kontalk.model.CoderStatus; import org.kontalk.model.MessageContent; import org.kontalk.model.Contact; +import org.kontalk.model.Member; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.Transmission; @@ -128,7 +129,7 @@ protected void updateOnEDT(Object arg) { if (arg instanceof Set || arg instanceof String || arg instanceof Boolean || - arg instanceof Chat.Member) { + arg instanceof Member) { // contacts, subject, read status or chat state changed, nothing // to do here return; From e0ca5cdce6eb3549412074ff1500438a3c5b7d8f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 2 Feb 2016 19:09:41 +0100 Subject: [PATCH 125/257] netbeans: time flies --- nbproject/licenseheader.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbproject/licenseheader.txt b/nbproject/licenseheader.txt index cf7fb787..829f1040 100644 --- a/nbproject/licenseheader.txt +++ b/nbproject/licenseheader.txt @@ -2,7 +2,7 @@ ${licenseFirst} ${licensePrefix} Kontalk Java client -${licensePrefix} Copyright (C) 2014 Kontalk Devteam +${licensePrefix} Copyright (C) 2016 Kontalk Devteam ${licensePrefix} ${licensePrefix} This program is free software: you can redistribute it and/or modify ${licensePrefix} it under the terms of the GNU General Public License as published by From c896a0bdb25f798897bcfddaf3defbda82efdc2f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 3 Feb 2016 21:31:07 +0100 Subject: [PATCH 126/257] model/control: add group owner as member with role 'owner' to new group chats --- src/main/java/org/kontalk/model/ChatList.java | 34 ++++-------------- .../java/org/kontalk/model/GroupChat.java | 6 +--- src/main/java/org/kontalk/system/Control.java | 30 ++++++++-------- .../java/org/kontalk/system/GroupControl.java | 35 +++++++++++++++++-- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index 720d665a..974a46fc 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -20,7 +20,6 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -89,22 +88,6 @@ public Optional get(Contact contact, String xmmpThreadID) { return Optional.empty(); } - /** Get group chat with group ID and containing contact. */ - public Optional get(GroupMetaData gData, Contact contact) { - for (Chat chat : mMap.values()) { - if (!(chat instanceof GroupChat)) - continue; - - GroupChat groupChat = (GroupChat) chat; - if (groupChat.getGroupData().equals(gData) && - groupChat.getAllContacts().contains(contact)) { - return Optional.of(groupChat); - } - } - - return Optional.empty(); - } - public Optional get(GroupMetaData gData) { for (Chat chat : mMap.values()) { if (!(chat instanceof GroupChat)) @@ -119,15 +102,6 @@ public Optional get(GroupMetaData gData) { return Optional.empty(); } - /** Find group chat by group data or create a new chat. */ - public GroupChat getOrCreate(GroupMetaData gData, Contact contact) { - GroupChat chat = this.get(gData, contact).orElse(null); - if (chat != null) - return chat;; - - return this.createNew(Arrays.asList(contact), gData, ""); - } - public Chat getOrCreate(Contact contact) { return this.getOrCreate(contact, ""); } @@ -149,8 +123,12 @@ private SingleChat createNew(Contact contact, String xmppThreadID) { return newChat; } - public GroupChat createNew(List contacts, GroupMetaData gData, String subject) { - GroupChat newChat = GroupChat.create(contacts, gData, subject); + public GroupChat create(List members, GroupMetaData gData) { + return createNew(members, gData, ""); + } + + public GroupChat createNew(List members, GroupMetaData gData, String subject) { + GroupChat newChat = GroupChat.create(members, gData, subject); LOGGER.config("new group chat: "+newChat); this.putSilent(newChat); this.changed(newChat); diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 3f3022da..2a4458f9 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -341,11 +341,7 @@ static GroupChat create(int id, List members, GroupMetaData gData, Strin new MUCChat(id, members, (MUCData) gData, subject, read, jsonViewSettings); } - public static GroupChat create(List contacts, GroupMetaData gData, String subject) { - List members = new ArrayList<>(contacts.size()); - for (Contact c : contacts) { - members.add(new Member(c)); - } + public static GroupChat create(List members, GroupMetaData gData, String subject) { return (gData instanceof KonGroupData) ? new KonGroupChat(members, (KonGroupData) gData, subject) : new MUCChat(members, (MUCData) gData, subject); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index d9c2bcd4..ba09e772 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -53,6 +53,7 @@ import org.kontalk.model.Avatar; import org.kontalk.model.GroupChat; import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.Member; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.ProtoMessage; @@ -169,14 +170,14 @@ public boolean onNewInMessage(MessageIDs ids, MessageContent content) { LOGGER.info("new incoming message, "+ids); - Contact contact = this.getOrCreateContact(ids.jid).orElse(null); - if (contact == null) { + Contact sender = this.getOrCreateContact(ids.jid).orElse(null); + if (sender == null) { LOGGER.warning("can't get contact for message"); return false; } // decrypt message now to get group id - ProtoMessage protoMessage = new ProtoMessage(contact, content); + ProtoMessage protoMessage = new ProtoMessage(sender, content); if (protoMessage.isEncrypted()) { Coder.decryptMessage(protoMessage); } @@ -184,11 +185,11 @@ public boolean onNewInMessage(MessageIDs ids, // NOTE: decryption must be successful to select group chat KonGroupData gData = protoMessage.getContent().getGroupData().orElse(null); - // TODO ignore message if it contains unexpected group commands - Chat chat = gData != null ? - ChatList.getInstance().getOrCreate(gData, contact) : - ChatList.getInstance().getOrCreate(contact, ids.xmppThreadID); + GroupControl.getGroupChat(gData, sender).orElse(null) : + ChatList.getInstance().getOrCreate(sender, ids.xmppThreadID); + if (chat == null) + return true; InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, ids.xmppID, serverDate); @@ -212,7 +213,7 @@ public boolean onNewInMessage(MessageIDs ids, if (com != null) { if (chat instanceof GroupChat) { mGroupControl.getInstanceFor((GroupChat) chat) - .onInMessage(com, contact); + .onInMessage(com, sender); } else { LOGGER.warning("group command for non-group chat"); } @@ -684,21 +685,22 @@ public Chat getOrCreateSingleChat(Contact contact) { } public void createGroupChat(List contacts, String subject) { + // user is part of the group + List members = new ArrayList<>(contacts.size()+1); + for (Contact c : contacts) { + members.add(new Member(c)); + } Contact me = ContactList.getInstance().getMe().orElse(null); if (me == null) { LOGGER.warning("can't find myself"); return; } + members.add(new Member(me, Member.Role.OWNER)); - // user should be part of the group - List withMe = new ArrayList<>(contacts); - withMe.add(me); - - // TODO KonGroupData gData = GroupControl.newKonGroupData(me.getJID()); //MUCData gData = GroupControl.newMUCGroupData(); - GroupChat chat = ChatList.getInstance().createNew(withMe, + GroupChat chat = ChatList.getInstance().createNew(members, gData, subject); diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 171bdaf3..1c651125 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -19,15 +19,17 @@ package org.kontalk.system; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.logging.Logger; import org.kontalk.misc.JID; +import org.kontalk.model.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.GroupChat; import org.kontalk.model.GroupChat.KonGroupChat; -import org.kontalk.model.GroupChat.MUCChat; import org.kontalk.model.GroupMetaData.KonGroupData; -import org.kontalk.model.GroupMetaData.MUCData; +import org.kontalk.model.Member; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.GroupCommand; @@ -135,6 +137,7 @@ public void onInMessage(GroupCommand command, Contact sender) { mChat.applyGroupCommand(command, sender); } } + ChatControl getInstanceFor(GroupChat chat) { // TODO return (chat instanceof KonGroupChat) ? @@ -148,4 +151,32 @@ static KonGroupData newKonGroupData(JID myJID) { org.jivesoftware.smack.util.StringUtils.randomString(8)); } + static Optional getGroupChat(KonGroupData gData, Contact sender) { + ChatList chatList = ChatList.getInstance(); + + // get old... + GroupChat chat = chatList.get(gData).orElse(null); + if (chat != null) { + if (!chat.getAllContacts().contains(sender)) { + LOGGER.warning("chat does not include sender: "+chat); + // TODO we should ask owner to confirm member list + return Optional.empty(); + } + return Optional.of(chat); + } + + // ...or create new + if (!gData.owner.equals(sender.getJID())) { + LOGGER.warning("sender is not owner for new group chat: "+gData); + return Optional.empty(); + } + + // NOTE: the message should include a CREATE or ADD group command + // if we are here (but all security checks passed so we continue) + + return Optional.of( + chatList.create( + Arrays.asList(new Member(sender, Member.Role.OWNER)), + gData)); + } } From 43beefdb4c47102d6277f76c835344151b95781e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 3 Feb 2016 21:40:37 +0100 Subject: [PATCH 127/257] model: better solution to get chat deletion --- src/main/java/org/kontalk/model/Chat.java | 6 ++++++ src/main/java/org/kontalk/model/ChatList.java | 4 ---- src/main/java/org/kontalk/view/ChatView.java | 4 +--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 17fcbd67..5ebce206 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -71,6 +71,7 @@ public abstract class Chat extends Observable implements Observer { private final ChatMessages mMessages; private boolean mRead; + private boolean mDeleted = false; private ViewSettings mViewSettings; @@ -252,6 +253,11 @@ void delete() { // chat itself db.execDelete(TABLE, mID); + mDeleted = true; + } + + public boolean isDeleted() { + return mDeleted; } protected void changed(Object arg) { diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/ChatList.java index 974a46fc..aa381a07 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/ChatList.java @@ -145,10 +145,6 @@ private void putSilent(Chat chat) { chat.addObserver(this); } - public boolean contains(int id) { - return mMap.containsKey(id); - } - public boolean contains(Contact contact) { return this.get(contact, "").isPresent(); } diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index d1293d5e..93d13c77 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -72,7 +72,6 @@ import org.apache.tika.Tika; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.model.Chat; -import org.kontalk.model.ChatList; import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; import org.kontalk.system.Config; @@ -387,8 +386,7 @@ public void run() { private void updateOnEDT(Object arg) { if (arg instanceof Chat) { Chat chat = (Chat) arg; - if (!ChatList.getInstance().contains(chat.getID())) { - // chat was deleted + if (chat.isDeleted()) { MessageList viewList = mChatCache.get(chat.getID()); if (viewList != null) { viewList.clearItems(); From 9585b81408a91934eba9c3ab36f75e866f693a89 Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Sun, 31 Jan 2016 13:53:33 +0000 Subject: [PATCH 128/257] Translated using Weblate (German) Currently translated at 98.3% (241 of 245 strings) --- src/main/resources/i18n/strings_de.properties | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index c5aea8c7..7035a165 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -299,3 +299,11 @@ s_0WUC=Nicht überprüft s_FDTC=Wenn Sie dies aktzeptieren, wird dieser Kontakt Ihren Onlinestatus sehen können. s_MFZY=Onlinestatusanfragen von anderen Nutzern automatisch genehmigen s_FAS6=Chataktivität (schreibt…) an andere Nutzer senden +s_TAG1=Nutzerprofil +s_84VP=Ihr Profil bearbeiten +s_4QXX=Dein Profilbild: +s_1R7I=Profil +s_O2UH=Ihr Nutzerprofil einstellen +s_4FTV=Avatarbilder herunterladen +s_YL1F=Herunterladen von Kontaktavatarbildern +s_6SC9=Schlüssel Nutzer ID: From cd1fb6359d675662a26a06e6c41b62e975f8b598 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 4 Feb 2016 16:16:46 +0100 Subject: [PATCH 129/257] client: always send received receipt for incoming messages --- .../org/kontalk/client/KonMessageListener.java | 6 +++--- src/main/java/org/kontalk/system/Control.java | 16 ++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 67432fbb..0ef49361 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -183,11 +183,11 @@ private void processChatMessage(Message m) { } // add message - boolean success = mControl.onNewInMessage(ids, optServerDate, content); + mControl.onNewInMessage(ids, optServerDate, content); - // on success, send a 'received' for a request (XEP-0184) + // send a 'received' for a receipt request (XEP-0184) DeliveryReceiptRequest request = DeliveryReceiptRequest.from(m); - if (request != null && success && !ids.xmppID.isEmpty()) { + if (request != null && !ids.xmppID.isEmpty()) { Message received = new Message(m.getFrom(), Message.Type.chat); received.addExtension(new DeliveryReceipt(ids.xmppID)); mClient.sendPacket(received); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index ba09e772..dcb3c477 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -163,9 +163,8 @@ public void onSecurityErrors(KonMessage message) { /** * All-in-one method for a new incoming message (except handling server * receipts): Create, save and process the message. - * @return true on success or message is a duplicate, false on unexpected failure */ - public boolean onNewInMessage(MessageIDs ids, + public void onNewInMessage(MessageIDs ids, Optional serverDate, MessageContent content) { LOGGER.info("new incoming message, "+ids); @@ -173,7 +172,7 @@ public boolean onNewInMessage(MessageIDs ids, Contact sender = this.getOrCreateContact(ids.jid).orElse(null); if (sender == null) { LOGGER.warning("can't get contact for message"); - return false; + return; } // decrypt message now to get group id @@ -189,24 +188,23 @@ public boolean onNewInMessage(MessageIDs ids, GroupControl.getGroupChat(gData, sender).orElse(null) : ChatList.getInstance().getOrCreate(sender, ids.xmppThreadID); if (chat == null) - return true; + return; InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, ids.xmppID, serverDate); if (newMessage.getID() <= 0) - return false; + return; // TODO implement equals() if (chat.getMessages().contains(newMessage)) { LOGGER.info("message already in chat, dropping this one"); - return true; + return; } - boolean added = chat.addMessage(newMessage); if (!added) { LOGGER.warning("can't add message to chat"); - return false; + return; } GroupCommand com = newMessage.getContent().getGroupCommand().orElse(null); @@ -222,8 +220,6 @@ public boolean onNewInMessage(MessageIDs ids, this.processContent(newMessage); mViewControl.changed(new ViewEvent.NewMessage(newMessage)); - - return newMessage.getID() >= -1; } public void onMessageSent(MessageIDs ids) { From c3e30624483041b939cba580594cc44e54323775 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 4 Feb 2016 16:24:20 +0100 Subject: [PATCH 130/257] view: new member list used in details dialog for group chats --- src/main/java/org/kontalk/model/Chat.java | 6 +- .../java/org/kontalk/model/GroupChat.java | 2 +- .../java/org/kontalk/model/SingleChat.java | 2 +- .../java/org/kontalk/view/ChatDetails.java | 12 +-- .../java/org/kontalk/view/ComponentUtils.java | 95 ++++++++++++++++--- src/main/java/org/kontalk/view/Utils.java | 21 +++- 6 files changed, 111 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 5ebce206..5841556b 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -163,11 +163,9 @@ public boolean isGroupChat() { return (this instanceof GroupChat); } - protected abstract List getAllMembers(); + public abstract List getAllMembers(); - /** Get all contacts (including deleted, blocked and user contact). - * TODO remove me - */ + /** Get all contacts (including deleted, blocked and user contact). */ public abstract List getAllContacts(); /** Get valid receiver contacts (without deleted and blocked). */ diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/GroupChat.java index 2a4458f9..fcc9ccf7 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/GroupChat.java @@ -74,7 +74,7 @@ private GroupChat(int id, } @Override - protected List getAllMembers() { + public List getAllMembers() { return new ArrayList<>(mMemberSet); } diff --git a/src/main/java/org/kontalk/model/SingleChat.java b/src/main/java/org/kontalk/model/SingleChat.java index fde78e0d..af825f42 100644 --- a/src/main/java/org/kontalk/model/SingleChat.java +++ b/src/main/java/org/kontalk/model/SingleChat.java @@ -62,7 +62,7 @@ public Contact getContact() { } @Override - protected List getAllMembers() { + public List getAllMembers() { return Arrays.asList(mMember); } diff --git a/src/main/java/org/kontalk/view/ChatDetails.java b/src/main/java/org/kontalk/view/ChatDetails.java index 7915479f..f29547a4 100644 --- a/src/main/java/org/kontalk/view/ChatDetails.java +++ b/src/main/java/org/kontalk/view/ChatDetails.java @@ -39,10 +39,10 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.kontalk.model.Chat; -import org.kontalk.model.Contact; import org.kontalk.model.GroupChat; +import org.kontalk.model.Member; import org.kontalk.util.Tr; -import org.kontalk.view.ComponentUtils.ParticipantsList; +import org.kontalk.view.ComponentUtils.MemberList; /** * Show and edit thread/chat settings. @@ -82,10 +82,10 @@ final class ChatDetails extends WebPanel { new WebLabel(Tr.tr("Subject:")), mSubjectField)); groupPanel.add(new WebLabel(Tr.tr("Participants:"))); - ParticipantsList mParticipantsList = new ParticipantsList(false); - List chatContacts = Utils.contactList(mChat); - mParticipantsList.setContacts(chatContacts); - mParticipantsList.setVisibleRowCount(Math.min(chatContacts.size(), 5)); + MemberList mParticipantsList = new MemberList(false); + List chatMember = Utils.memberList(mChat); + mParticipantsList.setMembers(chatMember); + mParticipantsList.setVisibleRowCount(Math.min(chatMember.size(), 5)); groupPanel.add(new ComponentUtils.ScrollPane(mParticipantsList, false).setPreferredWidth(160)); groupPanel.add(new WebSeparator(true, true)); diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 3fc57839..3a50a4c2 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -22,6 +22,7 @@ import com.alee.extended.label.WebLinkLabel; import com.alee.extended.layout.FormLayout; import com.alee.extended.panel.GroupPanel; +import com.alee.extended.panel.GroupingType; import com.alee.laf.button.WebButton; import com.alee.laf.button.WebToggleButton; import com.alee.laf.checkbox.WebCheckBox; @@ -66,6 +67,7 @@ import java.util.Optional; import javax.swing.AbstractButton; import javax.swing.BorderFactory; +import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.Icon; @@ -86,6 +88,7 @@ import javax.swing.text.PlainDocument; import org.kontalk.misc.JID; import org.kontalk.model.Contact; +import org.kontalk.model.Member; import org.kontalk.system.Config; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; @@ -396,12 +399,66 @@ static class ParticipantsList extends WebList { private final DefaultListModel mModel; - public ParticipantsList() { + @SuppressWarnings("unchecked") + ParticipantsList() { + mModel = new DefaultListModel<>(); + this.setModel(mModel); + this.setFixedCellHeight(25); + + this.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + this.setSelectionModel(new DefaultListSelectionModel() { + @Override + public void setSelectionInterval(int index0, int index1) { + if(super.isSelectedIndex(index0)) { + super.removeSelectionInterval(index0, index1); + } else { + super.addSelectionInterval(index0, index1); + } + } + }); + + this.setCellRenderer(new CellRenderer()); + } + + void setContacts(List contacts) { + mModel.clear(); + + for (Contact contact : contacts) + mModel.addElement(contact); + } + + @SuppressWarnings("unchecked") + List getSelectedContacts() { + return this.getSelectedValuesList(); + } + + private class CellRenderer extends WebLabel implements ListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, + Contact contact, + int index, + boolean isSelected, + boolean cellHasFocus) { + this.setText(" " + Utils.displayName(contact)); + + this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0,Color.LIGHT_GRAY)); + + return this; + } + } + } + + // NOTE: https://github.com/mgarin/weblaf/issues/153 + static class MemberList extends WebList { + + private final DefaultListModel mModel; + + public MemberList() { this(true); } @SuppressWarnings("unchecked") - ParticipantsList(boolean selectable) { + MemberList(boolean selectable) { mModel = new DefaultListModel<>(); this.setModel(mModel); this.setFixedCellHeight(25); @@ -424,28 +481,38 @@ public void setSelectionInterval(int index0, int index1) { this.setCellRenderer(new CellRenderer()); } - void setContacts(List contacts) { + void setMembers(List members) { mModel.clear(); - for (Contact contact : contacts) - mModel.addElement(contact); + for (Member member : members) + mModel.addElement(member); } - @SuppressWarnings("unchecked") - List getSelectedContacts() { - return this.getSelectedValuesList(); - } + private class CellRenderer extends WebPanel implements ListCellRenderer { + private final WebLabel mNameLabel; + private final WebLabel mRoleLabel; + + public CellRenderer() { + mNameLabel = new WebLabel(); + mRoleLabel = new WebLabel(); + mRoleLabel.setForeground(View.DARK_GREEN); + + this.setMargin(View.MARGIN_DEFAULT); + this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY)); + + this.add(new GroupPanel(GroupingType.fillMiddle, View.GAP_DEFAULT, + mNameLabel, Box.createGlue(), mRoleLabel), + BorderLayout.CENTER); + } - private class CellRenderer extends WebLabel implements ListCellRenderer { @Override public Component getListCellRendererComponent(JList list, - Contact contact, + Member member, int index, boolean isSelected, boolean cellHasFocus) { - this.setText(" " + Utils.displayName(contact)); - - this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0,Color.LIGHT_GRAY)); + mNameLabel.setText(Utils.displayName(member.getContact(), 25)); + mRoleLabel.setText(Utils.role(member.getRole())); return this; } diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 9620eef3..5631d4fd 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -61,6 +61,7 @@ import org.kontalk.model.Chat; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; +import org.kontalk.model.Member; import org.kontalk.system.Config; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; @@ -186,7 +187,7 @@ static String displayName(Contact contact) { return displayName(contact, Integer.MAX_VALUE); } - private static String displayName(Contact contact, int maxLength) { + static String displayName(Contact contact, int maxLength) { return displayName(contact, contact.getJID(), maxLength); } @@ -243,6 +244,13 @@ private static String group(String s) { return StringUtils.join(s.split("(?<=\\G.{" + 4 + "})"), " "); } + static String role(Member.Role role) { + switch (role) { + case OWNER : return "[" + Tr.tr("Group Owner") + "]"; + default: return ""; + } + } + static String mainStatus(Contact c, boolean pre) { Contact.Subscription subStatus = c.getSubScription(); return c.isMe() ? Tr.tr("Myself") : @@ -378,6 +386,17 @@ public int compare(Contact c1, Contact c2) { return contacts; } + static List memberList(Chat chat) { + List members = new ArrayList<>(chat.getAllMembers()); + members.sort(new Comparator() { + @Override + public int compare(Member m1, Member m2) { + return Utils.compareContacts(m1.getContact(), m2.getContact()); + } + }); + return members; + } + static int compareContacts(Contact c1, Contact c2) { if (c1.isMe()) return +1; if (c2.isMe()) return -1; From 5ded24d3753e2b2bf41c4a39c4bc170fa2670207 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 4 Feb 2016 21:15:41 +0100 Subject: [PATCH 131/257] model: fix chat saving --- src/main/java/org/kontalk/model/Chat.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/Chat.java index 5841556b..f673cdd2 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/Chat.java @@ -21,6 +21,7 @@ import java.awt.Color; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; @@ -208,7 +209,7 @@ protected void save(List members, String subject) { db.execUpdate(TABLE, set, mID); // get receiver for this chat - List oldMembers = this.getAllMembers(); + List oldMembers = new ArrayList<>(this.getAllMembers()); // save new members for (Member m : members) { From fbd69f1456bf4203ee5fe3fec5feff04d40c7df5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 5 Feb 2016 19:03:44 +0100 Subject: [PATCH 132/257] rework on main class again --- src/main/java/org/kontalk/Kontalk.java | 154 ++++++++++-------- src/main/java/org/kontalk/client/Client.java | 2 +- src/main/java/org/kontalk/model/Account.java | 4 +- src/main/java/org/kontalk/model/Avatar.java | 6 +- .../org/kontalk/system/AttachmentManager.java | 2 +- src/main/java/org/kontalk/system/Config.java | 2 +- src/main/java/org/kontalk/system/Control.java | 8 +- .../java/org/kontalk/system/Database.java | 7 +- 8 files changed, 101 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index e6e180d7..c8a97397 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -44,7 +44,6 @@ import org.kontalk.model.ChatList; import org.kontalk.model.ContactList; import org.kontalk.system.Control; -import org.kontalk.system.Control.ViewControl; import org.kontalk.util.CryptoUtils; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; @@ -58,29 +57,43 @@ public final class Kontalk { public static final String VERSION = "3.0.4"; - private static ServerSocket RUN_LOCK = null; - private static Path APP_DIR = null; + private static Kontalk INSTANCE = null; - ViewControl mControl = null; + private ServerSocket mRunLock = null; + private Path mAppDir = null; - Kontalk() { + private static void ensureInitialized() { // platform dependent configuration directory - this(Paths.get(System.getProperty("user.home"), + ensureInitialized(Paths.get(System.getProperty("user.home"), SystemUtils.IS_OS_WINDOWS ? "Kontalk" : ".kontalk")); } - Kontalk(Path appDir) { - APP_DIR = appDir.toAbsolutePath(); + static void ensureInitialized(Path appDir) { + if (INSTANCE != null) + return; + + INSTANCE = new Kontalk(appDir); + } + + public static Kontalk getInstance() { + if (INSTANCE == null) + throw new IllegalStateException("application not initialized"); + + return INSTANCE; + } + + private Kontalk(Path appDir) { + mAppDir = appDir.toAbsolutePath(); } - void start(boolean ui) { + int start(boolean ui) { // check if already running try { InetAddress addr = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); - RUN_LOCK = new ServerSocket(9871, 10, addr); + mRunLock = new ServerSocket(9871, 10, addr); } catch(java.net.BindException ex) { LOGGER.severe("already running"); - System.exit(2); + return 2; } catch(IOException ex) { LOGGER.log(Level.WARNING, "can't create socket", ex); } @@ -93,17 +106,17 @@ void start(boolean ui) { if (jVersion.startsWith("1.7")) { View.showWrongJavaVersionDialog(); LOGGER.severe("java too old: "+jVersion); - System.exit(-3); + return 3; } // create app directory - boolean created = APP_DIR.toFile().mkdirs(); + boolean created = mAppDir.toFile().mkdirs(); if (created) - LOGGER.info("created application directory: "+APP_DIR); + LOGGER.info("created application directory: "+mAppDir); - if (!Files.isWritable(APP_DIR)) { - LOGGER.severe("invalid app directory: "+APP_DIR); - return; + if (!Files.isWritable(mAppDir)) { + LOGGER.severe("invalid app directory: "+mAppDir); + return 4; } // logging @@ -113,7 +126,7 @@ void start(boolean ui) { if (h instanceof ConsoleHandler) h.setLevel(Level.CONFIG); } - String logPath = APP_DIR.resolve("debug.log").toString(); + String logPath = mAppDir.resolve("debug.log").toString(); Handler fileHandler = null; try { fileHandler = new FileHandler(logPath, 1024*1000, 1, true); @@ -134,28 +147,27 @@ void start(boolean ui) { // register provider PGPUtils.registerProvider(); - mControl = Control.create(); + try { + // do now to test if successful + Database.ensureInitialized(); + } catch (KonException ex) { + LOGGER.log(Level.SEVERE, "can't initialize database", ex); + return 5; + } + + final Control.ViewControl control = Control.create(); // handle shutdown signals Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { @Override public void run() { - // NOTE: logging does not work here anymore already - mControl.shutDown(false); - System.out.println("shutdown finished"); + // NOTE: logging does not work here anymore + control.shutDown(false); + System.out.println("Kontalk: shutdown finished"); } }); - View view = ui ? View.create(mControl).orElse(null) : null; - - try { - // do now to test if successful - Database.initialize(); - } catch (KonException ex) { - LOGGER.log(Level.SEVERE, "can't initialize database", ex); - mControl.shutDown(true); - return; // never reached - } + View view = ui ? View.create(control).orElse(null) : null; // order matters! ContactList.getInstance().load(); @@ -164,48 +176,27 @@ public void run() { if (view != null) view.init(); - mControl.launch(); + control.launch(); - new Thread("Kontalk Main") { - @Override - public void run() { - try { - // wait until exit call - Object lock = new Object(); - synchronized (lock) { - lock.wait(); - } - } catch (InterruptedException ex) { - LOGGER.log(Level.WARNING, "interrupted while waiting", ex); - } - } - }.start(); + return 0; } - void stop() { - if (mControl == null) { - LOGGER.warning("not started"); - return; - } - - mControl.shutDown(true); + public Path appDir() { + return mAppDir; } - public static Path appDir() { - if (APP_DIR == null) - throw new IllegalStateException("app dir not initialized"); - - return APP_DIR; - } - - public static void removeLock() { - if (RUN_LOCK != null) { - try { - RUN_LOCK.close(); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't close run socket", ex); - } + public boolean removeLock() { + if (mRunLock == null) { + LOGGER.warning("no lock"); + return false; } + try { + mRunLock.close(); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't close run socket", ex); + return false; + } + return true; } /** @@ -241,10 +232,29 @@ public static void main(String[] args) { String appDir = cmd.getOptionValue("d", ""); - Kontalk app = appDir.isEmpty() ? - new Kontalk() : - new Kontalk(Paths.get(appDir)); - app.start(!cmd.hasOption("c")); + if (!appDir.isEmpty()) + Kontalk.ensureInitialized(Paths.get(appDir)); + else + Kontalk.ensureInitialized(); + + int returnCode = Kontalk.getInstance().start(!cmd.hasOption("c")); + if (returnCode != 0) + System.exit(returnCode); + + new Thread("Kontalk Main") { + @Override + public void run() { + try { + // wait until exit call + Object lock = new Object(); + synchronized (lock) { + lock.wait(); + } + } catch (InterruptedException ex) { + LOGGER.log(Level.WARNING, "interrupted while waiting", ex); + } + } + }.start(); } private static void showHelp(Options options) { diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 3a926600..e6c9a663 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -97,7 +97,7 @@ private Client(Control control) { mFeatures = EnumSet.noneOf(ServerFeature.class); // setting caps cache - File cacheDir = Kontalk.appDir().resolve(CAPS_CACHE_DIR).toFile(); + File cacheDir = Kontalk.getInstance().appDir().resolve(CAPS_CACHE_DIR).toFile(); if (cacheDir.mkdir()) LOGGER.info("created caps cache directory"); diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index da635aa6..265a738f 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -45,7 +45,7 @@ /** * The user account. There can only be one. - * + * * @author Alexander Bikadorov {@literal } */ public final class Account { @@ -205,7 +205,7 @@ private boolean fileExists(String filename) { public synchronized static Account getInstance() { if (INSTANCE == null) { - INSTANCE = new Account(Kontalk.appDir(), Config.getInstance()); + INSTANCE = new Account(Kontalk.getInstance().appDir(), Config.getInstance()); } return INSTANCE; } diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 0d7f06f1..848dc0c2 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -63,7 +63,7 @@ private Avatar(String id, File file, BufferedImage image) { mID = id; mFile = file != null ? file : - Kontalk.appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); + Kontalk.getInstance().appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); mImage = image; if (mImage != null) { @@ -152,7 +152,7 @@ public Optional imageData() { } private static File userFile() { - return Kontalk.appDir().resolve(USER_FILENAME + "." + FORMAT).toFile(); + return Kontalk.getInstance().appDir().resolve(USER_FILENAME + "." + FORMAT).toFile(); } public static UserAvatar instance() { @@ -175,7 +175,7 @@ public static void deleteImage() { } public static void createDir() { - boolean created = Kontalk.appDir().resolve(DIR).toFile().mkdir(); + boolean created = Kontalk.getInstance().appDir().resolve(DIR).toFile().mkdir(); if (created) LOGGER.info("created avatar directory"); } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index c25a7e26..29337d6e 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -338,7 +338,7 @@ public static boolean isImage(String mimeType) { } static AttachmentManager create(Control control) { - AttachmentManager manager = new AttachmentManager(Kontalk.appDir(), control); + AttachmentManager manager = new AttachmentManager(Kontalk.getInstance().appDir(), control); Thread thread = new Thread(manager, "Attachment Transfer"); thread.setDaemon(true); diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index 958069bd..e0bcc36f 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -126,7 +126,7 @@ public void saveToFile() { public synchronized static Config getInstance() { if (INSTANCE == null) { - INSTANCE = new Config(Kontalk.appDir().resolve(Config.FILENAME)); + INSTANCE = new Config(Kontalk.getInstance().appDir().resolve(Config.FILENAME)); } return INSTANCE; } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index dcb3c477..161db9b8 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -30,6 +30,7 @@ import java.util.Observable; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; @@ -561,10 +562,13 @@ public void shutDown(boolean exit) { try { Database.getInstance().close(); } catch (RuntimeException ex) { - // ignore + LOGGER.log(Level.WARNING, "can't close database", ex); } + Config.getInstance().saveToFile(); - Kontalk.removeLock(); + + Kontalk.getInstance().removeLock(); + if (exit) { LOGGER.info("exit"); System.exit(0); diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 15bf8dd7..2a2472c2 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -364,8 +364,11 @@ public static String setString(String s) { return s.isEmpty() ? null : s; } - public static void initialize() throws KonException { - INSTANCE = new Database(Kontalk.appDir().resolve(Database.FILENAME)); + public static void ensureInitialized() throws KonException { + if (INSTANCE != null) + return; + + INSTANCE = new Database(Kontalk.getInstance().appDir().resolve(Database.FILENAME)); } public static Database getInstance() { From bed36f2a922d38130269e9e1e17a8211cacefdfd Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 5 Feb 2016 19:04:14 +0100 Subject: [PATCH 133/257] test: tests for main class added --- src/test/java/org/kontalk/KontalkTest.java | 120 +++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/test/java/org/kontalk/KontalkTest.java diff --git a/src/test/java/org/kontalk/KontalkTest.java b/src/test/java/org/kontalk/KontalkTest.java new file mode 100644 index 00000000..75026a0d --- /dev/null +++ b/src/test/java/org/kontalk/KontalkTest.java @@ -0,0 +1,120 @@ +/* + * Kontalk Java client + * Copyright (C) 2014 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.kontalk; + +import java.io.IOException; +import java.nio.file.Path; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Ignore; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +public class KontalkTest { + @ClassRule + public static TemporaryFolder TEMP_FOLDER = new TemporaryFolder(); + + private static Path APP_DIR; + + public KontalkTest() { + } + + @BeforeClass + public static void setUpClass() { + APP_DIR = TEMP_FOLDER.getRoot().toPath().resolve("app_dir"); + Kontalk.ensureInitialized(APP_DIR); + } + + @AfterClass + public static void tearDownClass() throws IOException { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of start method, of class Kontalk. + */ + @Test + public void testStart() { + System.out.println("start"); + int returnCode = Kontalk.getInstance().start(false); + assertEquals(returnCode, 0); + // TODO stop + } + + /** + * Test of appDir method, of class Kontalk. + */ + @Test + public void testAppDir() { + System.out.println("appDir"); + Path result = Kontalk.getInstance().appDir(); + assertEquals(result, APP_DIR); + } + + /** + * Test of removeLock method, of class Kontalk. + */ + @Test + public void testRemoveLock() { + System.out.println("removeLock"); + Kontalk.getInstance().start(false); + boolean succ = Kontalk.getInstance().removeLock(); + assertTrue(succ); + } + + /** + * Test of getInstance method, of class Kontalk. + */ + @Test + public void testGetInstance() { + System.out.println("getInstance"); + Kontalk result = Kontalk.getInstance(); + assertNotNull(result); + } + + /** + * Test of main method, of class Kontalk. + */ + @Test + @Ignore + public void testMain() { + System.out.println("main"); + String[] args = null; + Kontalk.main(args); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +} From 99e3a64b4b5eea82cb2855f9f87cdf761effe43a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 6 Feb 2016 16:41:43 +0100 Subject: [PATCH 134/257] model: refactoring: moved chat classes to sub package --- src/main/java/org/kontalk/Kontalk.java | 2 +- .../java/org/kontalk/client/KonMessageListener.java | 2 +- .../java/org/kontalk/client/KonMessageSender.java | 6 +++--- src/main/java/org/kontalk/model/ContactList.java | 2 +- src/main/java/org/kontalk/model/InMessage.java | 1 + src/main/java/org/kontalk/model/KonMessage.java | 3 ++- src/main/java/org/kontalk/model/MessageContent.java | 2 +- src/main/java/org/kontalk/model/OutMessage.java | 1 + src/main/java/org/kontalk/model/Transmission.java | 2 +- src/main/java/org/kontalk/model/{ => chat}/Chat.java | 5 ++++- .../java/org/kontalk/model/{ => chat}/ChatList.java | 7 +++++-- .../org/kontalk/model/{ => chat}/ChatMessages.java | 5 ++++- .../java/org/kontalk/model/{ => chat}/GroupChat.java | 9 ++++++--- .../org/kontalk/model/{ => chat}/GroupMetaData.java | 2 +- .../java/org/kontalk/model/{ => chat}/Member.java | 5 ++++- .../org/kontalk/model/{ => chat}/SingleChat.java | 3 ++- .../java/org/kontalk/system/ChatStateManager.java | 4 ++-- src/main/java/org/kontalk/system/Control.java | 12 ++++++------ src/main/java/org/kontalk/system/Database.java | 4 ++-- src/main/java/org/kontalk/system/GroupControl.java | 10 +++++----- src/main/java/org/kontalk/util/ClientUtils.java | 4 ++-- src/main/java/org/kontalk/view/AvatarLoader.java | 2 +- src/main/java/org/kontalk/view/ChatDetails.java | 6 +++--- src/main/java/org/kontalk/view/ChatListView.java | 10 +++++----- src/main/java/org/kontalk/view/ChatView.java | 2 +- src/main/java/org/kontalk/view/ComponentUtils.java | 2 +- src/main/java/org/kontalk/view/ContactListView.java | 2 +- src/main/java/org/kontalk/view/Content.java | 2 +- src/main/java/org/kontalk/view/MessageList.java | 4 ++-- src/main/java/org/kontalk/view/TrayManager.java | 2 +- src/main/java/org/kontalk/view/Utils.java | 4 ++-- src/main/java/org/kontalk/view/View.java | 4 ++-- 32 files changed, 75 insertions(+), 56 deletions(-) rename src/main/java/org/kontalk/model/{ => chat}/Chat.java (98%) rename src/main/java/org/kontalk/model/{ => chat}/ChatList.java (97%) rename src/main/java/org/kontalk/model/{ => chat}/ChatMessages.java (97%) rename src/main/java/org/kontalk/model/{ => chat}/GroupChat.java (97%) rename src/main/java/org/kontalk/model/{ => chat}/GroupMetaData.java (99%) rename src/main/java/org/kontalk/model/{ => chat}/Member.java (97%) rename src/main/java/org/kontalk/model/{ => chat}/SingleChat.java (98%) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index c8a97397..d53ea787 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -41,7 +41,7 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.lang.SystemUtils; import org.kontalk.crypto.PGPUtils; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.ContactList; import org.kontalk.system.Control; import org.kontalk.util.CryptoUtils; diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 0ef49361..74f24e84 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -43,7 +43,7 @@ import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.misc.JID; -import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 4483b9d4..0f7065c8 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -26,9 +26,9 @@ import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.crypto.Coder; import org.kontalk.misc.JID; -import org.kontalk.model.Chat; -import org.kontalk.model.GroupChat.KonGroupChat; -import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.Chat; +import org.kontalk.model.chat.GroupChat.KonGroupChat; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.KonMessage; import org.kontalk.model.MessageContent; import org.kontalk.model.OutMessage; diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index c648ebc6..414d65e1 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -98,7 +98,7 @@ public Optional create(JID jid, String name) { return Optional.of(newContact); } - Optional get(int id) { + public Optional get(int id) { Contact contact = mIDMap.get(id); if (contact == null) { LOGGER.warning("can't find contact with ID: " + id); diff --git a/src/main/java/org/kontalk/model/InMessage.java b/src/main/java/org/kontalk/model/InMessage.java index 6dd093d1..7fa96703 100644 --- a/src/main/java/org/kontalk/model/InMessage.java +++ b/src/main/java/org/kontalk/model/InMessage.java @@ -18,6 +18,7 @@ package org.kontalk.model; +import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; import java.util.Date; import java.util.Optional; diff --git a/src/main/java/org/kontalk/model/KonMessage.java b/src/main/java/org/kontalk/model/KonMessage.java index e9b70b31..3280e710 100644 --- a/src/main/java/org/kontalk/model/KonMessage.java +++ b/src/main/java/org/kontalk/model/KonMessage.java @@ -18,6 +18,7 @@ package org.kontalk.model; +import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; @@ -281,7 +282,7 @@ public int compareTo(KonMessage o) { return Integer.compare(mID, o.getID()); } - static KonMessage load(ResultSet messageRS, Chat chat) throws SQLException { + public static KonMessage load(ResultSet messageRS, Chat chat) throws SQLException { int id = messageRS.getInt("_id"); String xmppID = Database.getString(messageRS, KonMessage.COL_XMPP_ID); diff --git a/src/main/java/org/kontalk/model/MessageContent.java b/src/main/java/org/kontalk/model/MessageContent.java index 8a23f17d..5276f354 100644 --- a/src/main/java/org/kontalk/model/MessageContent.java +++ b/src/main/java/org/kontalk/model/MessageContent.java @@ -32,7 +32,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.crypto.Coder; -import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.util.EncodingUtils; /** diff --git a/src/main/java/org/kontalk/model/OutMessage.java b/src/main/java/org/kontalk/model/OutMessage.java index 563d6d74..bcbdd427 100644 --- a/src/main/java/org/kontalk/model/OutMessage.java +++ b/src/main/java/org/kontalk/model/OutMessage.java @@ -18,6 +18,7 @@ package org.kontalk.model; +import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; import java.net.URI; import java.util.Date; diff --git a/src/main/java/org/kontalk/model/Transmission.java b/src/main/java/org/kontalk/model/Transmission.java index 1c971e03..e33cca82 100644 --- a/src/main/java/org/kontalk/model/Transmission.java +++ b/src/main/java/org/kontalk/model/Transmission.java @@ -40,7 +40,7 @@ final public class Transmission { private static final Logger LOGGER = Logger.getLogger(Transmission.class.getName()); public static final String TABLE = "transmissions"; - static final String COL_MESSAGE_ID = "message_id"; + public static final String COL_MESSAGE_ID = "message_id"; private static final String COL_CONTACT_ID = "user_id"; private static final String COL_JID = "jid"; private static final String COL_REC_DATE = "received_date"; diff --git a/src/main/java/org/kontalk/model/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java similarity index 98% rename from src/main/java/org/kontalk/model/Chat.java rename to src/main/java/org/kontalk/model/chat/Chat.java index f673cdd2..010e0892 100644 --- a/src/main/java/org/kontalk/model/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; import java.awt.Color; import java.sql.ResultSet; @@ -36,6 +36,9 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.json.simple.JSONObject; import org.json.simple.JSONValue; +import org.kontalk.model.Contact; +import org.kontalk.model.KonMessage; +import org.kontalk.model.Transmission; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java similarity index 97% rename from src/main/java/org/kontalk/model/ChatList.java rename to src/main/java/org/kontalk/model/chat/ChatList.java index aa381a07..b551f737 100644 --- a/src/main/java/org/kontalk/model/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -16,8 +16,11 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; +import org.kontalk.model.chat.Chat; +import org.kontalk.model.chat.GroupChat; +import org.kontalk.model.chat.SingleChat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; @@ -32,7 +35,7 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import org.kontalk.model.GroupMetaData; +import org.kontalk.model.Contact; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java similarity index 97% rename from src/main/java/org/kontalk/model/ChatMessages.java rename to src/main/java/org/kontalk/model/chat/ChatMessages.java index 66d86a14..83aa7498 100644 --- a/src/main/java/org/kontalk/model/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; +import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; @@ -27,6 +28,8 @@ import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; +import org.kontalk.model.KonMessage; +import org.kontalk.model.OutMessage; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java similarity index 97% rename from src/main/java/org/kontalk/model/GroupChat.java rename to src/main/java/org/kontalk/model/chat/GroupChat.java index fcc9ccf7..6df0af00 100644 --- a/src/main/java/org/kontalk/model/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; import java.util.ArrayList; import java.util.HashSet; @@ -27,8 +27,11 @@ import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.misc.JID; -import org.kontalk.model.GroupMetaData.KonGroupData; -import org.kontalk.model.GroupMetaData.MUCData; +import org.kontalk.model.Contact; +import org.kontalk.model.ContactList; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.GroupMetaData.MUCData; +import org.kontalk.model.MessageContent; /** * A long-term persistent chat conversation with multiple participants. diff --git a/src/main/java/org/kontalk/model/GroupMetaData.java b/src/main/java/org/kontalk/model/chat/GroupMetaData.java similarity index 99% rename from src/main/java/org/kontalk/model/GroupMetaData.java rename to src/main/java/org/kontalk/model/chat/GroupMetaData.java index c2a277db..2a54dbae 100644 --- a/src/main/java/org/kontalk/model/GroupMetaData.java +++ b/src/main/java/org/kontalk/model/chat/GroupMetaData.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; import java.util.Map; import java.util.Objects; diff --git a/src/main/java/org/kontalk/model/Member.java b/src/main/java/org/kontalk/model/chat/Member.java similarity index 97% rename from src/main/java/org/kontalk/model/Member.java rename to src/main/java/org/kontalk/model/chat/Member.java index e36b4059..238b5576 100644 --- a/src/main/java/org/kontalk/model/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -16,8 +16,9 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; +import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; @@ -29,6 +30,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; +import org.kontalk.model.Contact; +import org.kontalk.model.ContactList; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java similarity index 98% rename from src/main/java/org/kontalk/model/SingleChat.java rename to src/main/java/org/kontalk/model/chat/SingleChat.java index af825f42..5bdc4f2c 100644 --- a/src/main/java/org/kontalk/model/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.chat; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; +import org.kontalk.model.Contact; /** * diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index 747ef6a6..61f049e6 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -25,9 +25,9 @@ import java.util.concurrent.TimeUnit; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.client.Client; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; -import org.kontalk.model.SingleChat; +import org.kontalk.model.chat.SingleChat; /** * Manager handling own chat status for all chats. diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 161db9b8..c503a35a 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -44,21 +44,21 @@ import org.kontalk.misc.ViewEvent; import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.MessageContent; import org.kontalk.model.OutMessage; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.misc.JID; import org.kontalk.model.Avatar; -import org.kontalk.model.GroupChat; -import org.kontalk.model.GroupMetaData.KonGroupData; -import org.kontalk.model.Member; +import org.kontalk.model.chat.GroupChat; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.Member; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.ProtoMessage; -import org.kontalk.model.SingleChat; +import org.kontalk.model.chat.SingleChat; import org.kontalk.util.ClientUtils.MessageIDs; import org.kontalk.util.XMPPUtils; diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 2a2472c2..841655e7 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -39,9 +39,9 @@ import org.kontalk.misc.JID; import org.kontalk.misc.KonException; import org.kontalk.model.KonMessage; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; -import org.kontalk.model.Member; +import org.kontalk.model.chat.Member; import org.kontalk.model.Transmission; import org.kontalk.util.EncodingUtils; import org.sqlite.SQLiteConfig; diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 1c651125..5e771dbc 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -24,12 +24,12 @@ import java.util.Optional; import java.util.logging.Logger; import org.kontalk.misc.JID; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; -import org.kontalk.model.GroupChat; -import org.kontalk.model.GroupChat.KonGroupChat; -import org.kontalk.model.GroupMetaData.KonGroupData; -import org.kontalk.model.Member; +import org.kontalk.model.chat.GroupChat; +import org.kontalk.model.chat.GroupChat.KonGroupChat; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.Member; import org.kontalk.model.MessageContent; import org.kontalk.model.MessageContent.GroupCommand; diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 779b4c0f..39981046 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -31,8 +31,8 @@ import org.kontalk.client.GroupExtension.Member; import org.kontalk.model.Contact; import org.kontalk.misc.JID; -import org.kontalk.model.GroupChat.KonGroupChat; -import org.kontalk.model.GroupMetaData.KonGroupData; +import org.kontalk.model.chat.GroupChat.KonGroupChat; +import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.MessageContent.GroupCommand.OP; diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index e915ecca..b85288fa 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -30,7 +30,7 @@ import java.util.Objects; import org.apache.commons.lang.ObjectUtils; import org.kontalk.model.Avatar; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.util.MediaUtils; diff --git a/src/main/java/org/kontalk/view/ChatDetails.java b/src/main/java/org/kontalk/view/ChatDetails.java index f29547a4..236bd78d 100644 --- a/src/main/java/org/kontalk/view/ChatDetails.java +++ b/src/main/java/org/kontalk/view/ChatDetails.java @@ -38,9 +38,9 @@ import java.util.List; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; -import org.kontalk.model.Chat; -import org.kontalk.model.GroupChat; -import org.kontalk.model.Member; +import org.kontalk.model.chat.Chat; +import org.kontalk.model.chat.GroupChat; +import org.kontalk.model.chat.Member; import org.kontalk.util.Tr; import org.kontalk.view.ComponentUtils.MemberList; diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 6cd8e423..5bee53ea 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -38,13 +38,13 @@ import javax.swing.event.ListSelectionListener; import org.kontalk.system.Config; import org.kontalk.model.KonMessage; -import org.kontalk.model.Chat; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.Chat; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; -import org.kontalk.model.GroupChat; -import org.kontalk.model.Member; +import org.kontalk.model.chat.GroupChat; +import org.kontalk.model.chat.Member; import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.SingleChat; +import org.kontalk.model.chat.SingleChat; import org.kontalk.util.Tr; import org.kontalk.view.ChatListView.ChatItem; diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 93d13c77..06b25267 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -71,7 +71,7 @@ import org.apache.commons.io.FileUtils; import org.apache.tika.Tika; import org.jivesoftware.smackx.chatstates.ChatState; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; import org.kontalk.system.Config; diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 3a50a4c2..2effdad7 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -88,7 +88,7 @@ import javax.swing.text.PlainDocument; import org.kontalk.misc.JID; import org.kontalk.model.Contact; -import org.kontalk.model.Member; +import org.kontalk.model.chat.Member; import org.kontalk.system.Config; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 9c6c33cc..ae084bb0 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -38,7 +38,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.StringEscapeUtils; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.system.Control; diff --git a/src/main/java/org/kontalk/view/Content.java b/src/main/java/org/kontalk/view/Content.java index 31abbe4b..ed196505 100644 --- a/src/main/java/org/kontalk/view/Content.java +++ b/src/main/java/org/kontalk/view/Content.java @@ -24,7 +24,7 @@ import java.awt.Component; import java.awt.GridBagLayout; import java.util.Optional; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; /** diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 3f6e1350..1cbe74f9 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -59,11 +59,11 @@ import org.kontalk.crypto.Coder; import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.CoderStatus; import org.kontalk.model.MessageContent; import org.kontalk.model.Contact; -import org.kontalk.model.Member; +import org.kontalk.model.chat.Member; import org.kontalk.model.MessageContent.Attachment; import org.kontalk.model.MessageContent.GroupCommand; import org.kontalk.model.Transmission; diff --git a/src/main/java/org/kontalk/view/TrayManager.java b/src/main/java/org/kontalk/view/TrayManager.java index 57a915ca..ab1c02a6 100644 --- a/src/main/java/org/kontalk/view/TrayManager.java +++ b/src/main/java/org/kontalk/view/TrayManager.java @@ -35,7 +35,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.ChatList; import org.kontalk.system.Config; import org.kontalk.util.Tr; diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 5631d4fd..0759a22a 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -58,10 +58,10 @@ import org.jxmpp.util.XmppStringUtils; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; -import org.kontalk.model.Chat; +import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; -import org.kontalk.model.Member; +import org.kontalk.model.chat.Member; import org.kontalk.system.Config; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 3410d8b5..dc50f6fd 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -46,8 +46,8 @@ import org.kontalk.client.Client; import org.kontalk.system.Config; import org.kontalk.misc.ViewEvent; -import org.kontalk.model.Chat; -import org.kontalk.model.ChatList; +import org.kontalk.model.chat.Chat; +import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.system.Control; From 67da8c1a5772d84520fcc11d941c46eee93a9f4a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 6 Feb 2016 16:44:45 +0100 Subject: [PATCH 135/257] model: refactoring: moved message classes to sub package --- src/main/java/org/kontalk/client/Client.java | 2 +- .../org/kontalk/client/KonMessageListener.java | 8 ++++---- .../java/org/kontalk/client/KonMessageSender.java | 8 ++++---- src/main/java/org/kontalk/crypto/Coder.java | 6 +++--- src/main/java/org/kontalk/crypto/Decryptor.java | 6 +++--- src/main/java/org/kontalk/crypto/Encryptor.java | 6 +++--- src/main/java/org/kontalk/misc/ViewEvent.java | 4 ++-- src/main/java/org/kontalk/model/chat/Chat.java | 4 ++-- .../java/org/kontalk/model/chat/ChatMessages.java | 4 ++-- .../java/org/kontalk/model/chat/GroupChat.java | 2 +- .../kontalk/model/{ => message}/CoderStatus.java | 2 +- .../model/{ => message}/DecryptMessage.java | 3 ++- .../org/kontalk/model/{ => message}/InMessage.java | 7 ++++--- .../kontalk/model/{ => message}/KonMessage.java | 4 ++-- .../model/{ => message}/MessageContent.java | 2 +- .../kontalk/model/{ => message}/OutMessage.java | 3 ++- .../kontalk/model/{ => message}/ProtoMessage.java | 3 ++- .../kontalk/model/{ => message}/Transmission.java | 4 +++- .../java/org/kontalk/system/AttachmentManager.java | 12 ++++++------ src/main/java/org/kontalk/system/Control.java | 14 +++++++------- src/main/java/org/kontalk/system/Database.java | 4 ++-- src/main/java/org/kontalk/system/GroupControl.java | 4 ++-- src/main/java/org/kontalk/util/ClientUtils.java | 4 ++-- src/main/java/org/kontalk/view/ChatListView.java | 4 ++-- src/main/java/org/kontalk/view/MessageList.java | 14 +++++++------- src/main/java/org/kontalk/view/Notifier.java | 4 ++-- 26 files changed, 72 insertions(+), 66 deletions(-) rename src/main/java/org/kontalk/model/{ => message}/CoderStatus.java (99%) rename src/main/java/org/kontalk/model/{ => message}/DecryptMessage.java (94%) rename src/main/java/org/kontalk/model/{ => message}/InMessage.java (95%) rename src/main/java/org/kontalk/model/{ => message}/KonMessage.java (99%) rename src/main/java/org/kontalk/model/{ => message}/MessageContent.java (99%) rename src/main/java/org/kontalk/model/{ => message}/OutMessage.java (98%) rename src/main/java/org/kontalk/model/{ => message}/ProtoMessage.java (97%) rename src/main/java/org/kontalk/model/{ => message}/Transmission.java (98%) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index e6c9a663..7139b7b5 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -54,7 +54,7 @@ import org.kontalk.misc.KonException; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.JID; -import org.kontalk.model.OutMessage; +import org.kontalk.model.message.OutMessage; import org.kontalk.system.Control; import org.kontalk.system.RosterHandler; diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 74f24e84..4d5cd43b 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -44,10 +44,10 @@ import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.misc.JID; import org.kontalk.model.chat.GroupMetaData.KonGroupData; -import org.kontalk.model.MessageContent; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.MessageContent.Preview; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.MessageContent.Attachment; +import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.system.Control; import org.kontalk.util.ClientUtils; import org.kontalk.util.ClientUtils.MessageIDs; diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 0f7065c8..ef33269e 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -29,10 +29,10 @@ import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; -import org.kontalk.model.KonMessage; -import org.kontalk.model.MessageContent; -import org.kontalk.model.OutMessage; -import org.kontalk.model.Transmission; +import org.kontalk.model.message.KonMessage; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.OutMessage; +import org.kontalk.model.message.Transmission; import org.kontalk.system.Control; import org.kontalk.util.ClientUtils; import org.kontalk.util.EncodingUtils; diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index 95809aca..206207b3 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -26,9 +26,9 @@ import java.util.logging.Logger; import org.kontalk.crypto.PGPUtils.PGPCoderKey; import org.kontalk.model.Contact; -import org.kontalk.model.OutMessage; -import org.kontalk.model.DecryptMessage; -import org.kontalk.model.InMessage; +import org.kontalk.model.message.OutMessage; +import org.kontalk.model.message.DecryptMessage; +import org.kontalk.model.message.InMessage; import org.kontalk.model.Account; /** diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index ce382d17..4702c944 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -53,9 +53,9 @@ import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.packet.Message; import org.kontalk.client.KonMessageListener; -import org.kontalk.model.MessageContent; -import org.kontalk.model.DecryptMessage; -import org.kontalk.model.InMessage; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.DecryptMessage; +import org.kontalk.model.message.InMessage; import org.kontalk.util.CPIMMessage; import org.kontalk.util.XMPPUtils; import org.xmlpull.v1.XmlPullParserException; diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index 2f88265d..b84c7888 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -50,9 +50,9 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; import org.kontalk.model.Contact; -import org.kontalk.model.MessageContent; -import org.kontalk.model.OutMessage; -import org.kontalk.model.Transmission; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.OutMessage; +import org.kontalk.model.message.Transmission; import org.kontalk.util.CPIMMessage; /** diff --git a/src/main/java/org/kontalk/misc/ViewEvent.java b/src/main/java/org/kontalk/misc/ViewEvent.java index e6c41479..0ec4fe8e 100644 --- a/src/main/java/org/kontalk/misc/ViewEvent.java +++ b/src/main/java/org/kontalk/misc/ViewEvent.java @@ -21,8 +21,8 @@ import java.util.EnumSet; import org.kontalk.client.Client; import org.kontalk.crypto.PGPUtils.PGPCoderKey; -import org.kontalk.model.InMessage; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.model.Contact; import org.kontalk.system.Control; import org.kontalk.system.RosterHandler; diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 010e0892..99874267 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -37,8 +37,8 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.model.Contact; -import org.kontalk.model.KonMessage; -import org.kontalk.model.Transmission; +import org.kontalk.model.message.KonMessage; +import org.kontalk.model.message.Transmission; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 83aa7498..23eaf0d0 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -28,8 +28,8 @@ import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; -import org.kontalk.model.KonMessage; -import org.kontalk.model.OutMessage; +import org.kontalk.model.message.KonMessage; +import org.kontalk.model.message.OutMessage; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 6df0af00..1c4d21a0 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -31,7 +31,7 @@ import org.kontalk.model.ContactList; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.GroupMetaData.MUCData; -import org.kontalk.model.MessageContent; +import org.kontalk.model.message.MessageContent; /** * A long-term persistent chat conversation with multiple participants. diff --git a/src/main/java/org/kontalk/model/CoderStatus.java b/src/main/java/org/kontalk/model/message/CoderStatus.java similarity index 99% rename from src/main/java/org/kontalk/model/CoderStatus.java rename to src/main/java/org/kontalk/model/message/CoderStatus.java index aa4c2d17..07aac217 100644 --- a/src/main/java/org/kontalk/model/CoderStatus.java +++ b/src/main/java/org/kontalk/model/message/CoderStatus.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import java.util.EnumSet; import org.kontalk.crypto.Coder; diff --git a/src/main/java/org/kontalk/model/DecryptMessage.java b/src/main/java/org/kontalk/model/message/DecryptMessage.java similarity index 94% rename from src/main/java/org/kontalk/model/DecryptMessage.java rename to src/main/java/org/kontalk/model/message/DecryptMessage.java index d052be25..bdd51c6d 100644 --- a/src/main/java/org/kontalk/model/DecryptMessage.java +++ b/src/main/java/org/kontalk/model/message/DecryptMessage.java @@ -16,11 +16,12 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import java.util.EnumSet; import org.kontalk.crypto.Coder; import org.kontalk.crypto.Coder.Signing; +import org.kontalk.model.Contact; /** * Interface for decryptable messages. diff --git a/src/main/java/org/kontalk/model/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java similarity index 95% rename from src/main/java/org/kontalk/model/InMessage.java rename to src/main/java/org/kontalk/model/message/InMessage.java index 7fa96703..b20c5134 100644 --- a/src/main/java/org/kontalk/model/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; @@ -24,8 +24,9 @@ import java.util.Optional; import java.util.logging.Logger; import org.kontalk.crypto.Coder; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.Preview; +import org.kontalk.model.Contact; +import org.kontalk.model.message.MessageContent.Attachment; +import org.kontalk.model.message.MessageContent.Preview; /** * Model for an XMPP message that was sent to us. diff --git a/src/main/java/org/kontalk/model/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java similarity index 99% rename from src/main/java/org/kontalk/model/KonMessage.java rename to src/main/java/org/kontalk/model/message/KonMessage.java index 3280e710..445e8d6a 100644 --- a/src/main/java/org/kontalk/model/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import org.kontalk.model.chat.Chat; import java.sql.ResultSet; @@ -36,7 +36,7 @@ import org.json.simple.JSONValue; import org.kontalk.system.Database; import org.kontalk.crypto.Coder; -import org.kontalk.model.MessageContent.Preview; +import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.util.EncodingUtils; /** diff --git a/src/main/java/org/kontalk/model/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java similarity index 99% rename from src/main/java/org/kontalk/model/MessageContent.java rename to src/main/java/org/kontalk/model/message/MessageContent.java index 5276f354..3bfbd83d 100644 --- a/src/main/java/org/kontalk/model/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import org.kontalk.misc.JID; import java.net.URI; diff --git a/src/main/java/org/kontalk/model/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java similarity index 98% rename from src/main/java/org/kontalk/model/OutMessage.java rename to src/main/java/org/kontalk/model/message/OutMessage.java index bcbdd427..07946e27 100644 --- a/src/main/java/org/kontalk/model/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; @@ -27,6 +27,7 @@ import java.util.Set; import java.util.logging.Logger; import org.jivesoftware.smack.util.StringUtils; +import org.kontalk.model.Contact; /** * Model for an XMPP message that we are sending. diff --git a/src/main/java/org/kontalk/model/ProtoMessage.java b/src/main/java/org/kontalk/model/message/ProtoMessage.java similarity index 97% rename from src/main/java/org/kontalk/model/ProtoMessage.java rename to src/main/java/org/kontalk/model/message/ProtoMessage.java index b4a5c0ce..6729a0bb 100644 --- a/src/main/java/org/kontalk/model/ProtoMessage.java +++ b/src/main/java/org/kontalk/model/message/ProtoMessage.java @@ -16,10 +16,11 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import java.util.EnumSet; import org.kontalk.crypto.Coder; +import org.kontalk.model.Contact; /** * An incoming message not saved to database for decryption. diff --git a/src/main/java/org/kontalk/model/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java similarity index 98% rename from src/main/java/org/kontalk/model/Transmission.java rename to src/main/java/org/kontalk/model/message/Transmission.java index e33cca82..7e052b66 100644 --- a/src/main/java/org/kontalk/model/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.model; +package org.kontalk.model.message; import org.kontalk.misc.JID; import java.sql.ResultSet; @@ -30,6 +30,8 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import org.kontalk.model.Contact; +import org.kontalk.model.ContactList; import org.kontalk.system.Database; /** diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 29337d6e..1b1ca1e0 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -41,12 +41,12 @@ import org.kontalk.crypto.Coder.Encryption; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; -import org.kontalk.model.InMessage; -import org.kontalk.model.KonMessage; -import org.kontalk.model.MessageContent; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.Preview; -import org.kontalk.model.OutMessage; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.KonMessage; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.MessageContent.Attachment; +import org.kontalk.model.message.MessageContent.Preview; +import org.kontalk.model.message.OutMessage; import org.kontalk.util.MediaUtils; /** diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index c503a35a..2c5e4d31 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -42,11 +42,11 @@ import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; import org.kontalk.misc.ViewEvent; -import org.kontalk.model.InMessage; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; -import org.kontalk.model.MessageContent; -import org.kontalk.model.OutMessage; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.OutMessage; import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.ContactList; @@ -55,9 +55,9 @@ import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.Member; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.ProtoMessage; +import org.kontalk.model.message.MessageContent.Attachment; +import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.model.message.ProtoMessage; import org.kontalk.model.chat.SingleChat; import org.kontalk.util.ClientUtils.MessageIDs; import org.kontalk.util.XMPPUtils; diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 841655e7..aeba97cb 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -38,11 +38,11 @@ import org.kontalk.Kontalk; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.model.chat.Member; -import org.kontalk.model.Transmission; +import org.kontalk.model.message.Transmission; import org.kontalk.util.EncodingUtils; import org.sqlite.SQLiteConfig; diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 5e771dbc..47eae986 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -30,8 +30,8 @@ import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.Member; -import org.kontalk.model.MessageContent; -import org.kontalk.model.MessageContent.GroupCommand; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.MessageContent.GroupCommand; /** * diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 39981046..36d99a9c 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -33,8 +33,8 @@ import org.kontalk.misc.JID; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; -import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.MessageContent.GroupCommand.OP; +import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.model.message.MessageContent.GroupCommand.OP; /** * diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 5bee53ea..4b99e668 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -37,13 +37,13 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.kontalk.system.Config; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; -import org.kontalk.model.MessageContent.GroupCommand; +import org.kontalk.model.message.MessageContent.GroupCommand; import org.kontalk.model.chat.SingleChat; import org.kontalk.util.Tr; import org.kontalk.view.ChatListView.ChatItem; diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 1cbe74f9..c3b086b9 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -57,16 +57,16 @@ import javax.swing.text.StyledEditorKit; import javax.swing.text.ViewFactory; import org.kontalk.crypto.Coder; -import org.kontalk.model.InMessage; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; -import org.kontalk.model.CoderStatus; -import org.kontalk.model.MessageContent; +import org.kontalk.model.message.CoderStatus; +import org.kontalk.model.message.MessageContent; import org.kontalk.model.Contact; import org.kontalk.model.chat.Member; -import org.kontalk.model.MessageContent.Attachment; -import org.kontalk.model.MessageContent.GroupCommand; -import org.kontalk.model.Transmission; +import org.kontalk.model.message.MessageContent.Attachment; +import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.model.message.Transmission; import org.kontalk.util.Tr; import org.kontalk.view.ChatView.Background; import org.kontalk.view.ComponentUtils.AttachmentPanel; diff --git a/src/main/java/org/kontalk/view/Notifier.java b/src/main/java/org/kontalk/view/Notifier.java index 265b5e05..b0c5fcc9 100644 --- a/src/main/java/org/kontalk/view/Notifier.java +++ b/src/main/java/org/kontalk/view/Notifier.java @@ -43,8 +43,8 @@ import org.kontalk.misc.KonException; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Contact; -import org.kontalk.model.InMessage; -import org.kontalk.model.KonMessage; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.KonMessage; import org.kontalk.system.RosterHandler; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; From dd0499eeb398b55438fd8b4677ab9b7ea6529516 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 7 Feb 2016 17:56:24 +0100 Subject: [PATCH 136/257] i18n: new language (after request): Arabic --- src/main/resources/i18n/strings_ar.properties | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/i18n/strings_ar.properties diff --git a/src/main/resources/i18n/strings_ar.properties b/src/main/resources/i18n/strings_ar.properties new file mode 100644 index 00000000..e69de29b From 8e0eb71d7342ccef54fb1dca49d0dabe6f01455b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 8 Feb 2016 17:12:49 +0100 Subject: [PATCH 137/257] updated client-common-java; group extension --- client-common-java | 2 +- .../kontalk/client/KonMessageListener.java | 2 +- .../java/org/kontalk/util/ClientUtils.java | 36 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/client-common-java b/client-common-java index 7d18a659..815ce670 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit 7d18a6595a47375672102d0fcd4a82e1ca56e1ed +Subproject commit 815ce6703fe6fc9678440f9932e395eef956081e diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 4d5cd43b..95cbe65f 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -274,7 +274,7 @@ public static MessageContent parseMessageContent(Message m) { GroupExtension group = (GroupExtension) groupExt; gid = new KonGroupData(JID.bare(group.getOwner()), group.getID()); groupCommand = ClientUtils.groupExtensionToGroupCommand( - group.getCommand(), group.getMember(), group.getSubject()).orElse(null); + group.getType(), group.getMembers(), group.getSubject()).orElse(null); } return new MessageContent.Builder(plainText, encrypted) diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 36d99a9c..a30a1b33 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -27,7 +27,7 @@ import org.apache.commons.lang.StringUtils; import org.jivesoftware.smack.packet.Message; import org.kontalk.client.GroupExtension; -import org.kontalk.client.GroupExtension.Command; +import org.kontalk.client.GroupExtension.Type; import org.kontalk.client.GroupExtension.Member; import org.kontalk.model.Contact; import org.kontalk.misc.JID; @@ -94,33 +94,34 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, switch (op) { case LEAVE: // weare leaving - return new GroupExtension(gid.id, gid.owner.string(), Command.LEAVE); + return new GroupExtension(gid.id, gid.owner.string(), Type.PART); case CREATE: case SET: - Command command; - Set member = new HashSet<>(); + default: + Type command; + Set members = new HashSet<>(); String subject = groupCommand.getSubject(); if (op == OP.CREATE) { - command = Command.CREATE; + command = Type.CREATE; for (JID added : groupCommand.getAdded()) - member.add(new Member(added.string())); + members.add(new Member(added.string())); } else { - command = Command.SET; + command = Type.SET; Set incl = new HashSet<>(); for (JID added : groupCommand.getAdded()) { incl.add(added); - member.add(new Member(added.string(), Member.Type.ADD)); + members.add(new Member(added.string(), Member.Operation.ADD)); } for (JID removed : groupCommand.getRemoved()) { incl.add(removed); - member.add(new Member(removed.string(), Member.Type.REMOVE)); + members.add(new Member(removed.string(), Member.Operation.REMOVE)); } if (groupCommand.getAdded().length > 0) { // list all remaining member for the new member for (Contact c : chat.getValidContacts()) { JID old = c.getJID(); if (!incl.contains(old)) - member.add(new Member(old.string())); + members.add(new Member(old.string())); } } } @@ -128,29 +129,26 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, return new GroupExtension(gid.id, gid.owner.string(), command, - member.toArray(new Member[0]), - subject); - default: - // can not happen - return null; + subject, + members); } } /* External to internal */ public static Optional groupExtensionToGroupCommand( - Command com, - Member[] members, + Type com, + List members, String subject) { switch (com) { case NONE: return Optional.empty(); case CREATE: - List jids = new ArrayList<>(members.length); + List jids = new ArrayList<>(members.size()); for (Member m: members) jids.add(JID.bare(m.jid)); return Optional.of(GroupCommand.create(jids.toArray(new JID[0]), subject)); - case LEAVE: + case PART: return Optional.of(GroupCommand.leave()); case SET: // TODO From f8d3a733b110ea68b7b2fad749a156d08992624a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 9 Feb 2016 19:55:48 +0100 Subject: [PATCH 138/257] view: made link detection in message text a bit less agressive --- src/main/java/org/kontalk/view/LinkUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/view/LinkUtils.java b/src/main/java/org/kontalk/view/LinkUtils.java index e37c1f1c..49204fc7 100644 --- a/src/main/java/org/kontalk/view/LinkUtils.java +++ b/src/main/java/org/kontalk/view/LinkUtils.java @@ -54,12 +54,12 @@ final class LinkUtils { = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); /** Undoubtedly the best URL regex ever made. */ private static final Pattern URL_PATTERN = Pattern.compile( - "(http[s]?://)?" + // scheme - "(\\w+(-+\\w+)*\\.)+" + // sub- and host-level(s) - "[a-z]{2,}(:[0-9]+)?" + // TLD and port - "(/[^\\s?#/]*)*" + // path - "(\\?[^\\s?#]*)*" + // query - "(\\#[^\\s?#]*)*", // fragment + "(http[s]?://)?" + // scheme; group 1 + "(\\w+[a-zA-Z_0-9-]*\\w+\\.)+" + // sub- and host-level(s); group 2 + "[a-z]{2,}(:[0-9]+)?" + // TLD and port; group 3 + "(/[^\\s?#/]*)*" + // path; group 4 + "(\\?[^\\s?#]*)*" + // query; group 5 + "(\\#[^\\s?#]*)*", // fragment; group 6 Pattern.CASE_INSENSITIVE); private static final String URL_ATT_NAME = "URL"; From ff60099d37a066b56d643342e04412043d05039d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 9 Feb 2016 19:57:39 +0100 Subject: [PATCH 139/257] view: show sent date instead of received date for incoming messages --- src/main/java/org/kontalk/view/MessageList.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index c3b086b9..806c9929 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -310,7 +310,11 @@ private void createContent() { } mStatusPanel.add(encryptIconLabel); // date label - WebLabel dateLabel = new WebLabel(Utils.SHORT_DATE_FORMAT.format(mValue.getDate())); + Date statusDate = mValue.isInMessage() ? + mValue.getServerDate().orElse(mValue.getDate()) : + mValue.getDate(); + WebLabel dateLabel = new WebLabel( + Utils.SHORT_DATE_FORMAT.format(statusDate)); dateLabel.setForeground(Color.GRAY); dateLabel.setFontSize(View.FONT_SIZE_TINY); mStatusPanel.add(dateLabel); From dfc02d263528f41021b4ead441579931e844b0eb Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Sun, 7 Feb 2016 12:40:35 +0000 Subject: [PATCH 140/257] Translated using Weblate (German) Currently translated at 98.3% (245 of 249 strings) --- src/main/resources/i18n/strings_de.properties | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index 7035a165..b7918bb4 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -307,3 +307,9 @@ s_O2UH=Ihr Nutzerprofil einstellen s_4FTV=Avatarbilder herunterladen s_YL1F=Herunterladen von Kontaktavatarbildern s_6SC9=Schlüssel Nutzer ID: +s_6K2I=Profilbilder herunterladen +s_08GL=Kontaktprofilbilder herunterladen +s_T2TO=Sich selbst in den Kontakten anzeigen +s_F1X0=Sich selbst in der Kontaktliste anzeigen +s_3PFR=Entfernen +s_D7FH=Vom Server nicht unterstützt From 314eb0f86095ce1998aa806cafa0cc186d9a45fe Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 12 Feb 2016 16:45:18 +0100 Subject: [PATCH 141/257] util: return extension for MIME without dot --- src/main/java/org/kontalk/client/HTTPFileClient.java | 2 +- src/main/java/org/kontalk/system/AttachmentManager.java | 6 +++--- src/main/java/org/kontalk/util/MediaUtils.java | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index 320b83e0..b197b6e6 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -160,7 +160,7 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx String ext = MediaUtils.extensionForMIME(type); filename = "att_" + org.jivesoftware.smack.util.StringUtils.randomString(4) + - ext; + "." + ext; } // get file size diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 1b1ca1e0..2435e19c 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -237,8 +237,8 @@ public void savePreview(InMessage message) { return; } String id = Integer.toString(message.getID()); - String dotExt = MediaUtils.extensionForMIME(preview.getMimeType()); - String filename = id + "_bob" + dotExt; + String ext = MediaUtils.extensionForMIME(preview.getMimeType()); + String filename = id + "_bob." + ext; this.writePreview(preview, filename); message.setPreviewFilename(filename); @@ -265,7 +265,7 @@ boolean createImagePreview(KonMessage message) { THUMBNAIL_DIM.height, false); - String format = MediaUtils.extensionForMIME(THUMBNAIL_MIME).substring(1); + String format = MediaUtils.extensionForMIME(THUMBNAIL_MIME); byte[] bytes = MediaUtils.imageToByteArray(thumb, format); if (bytes.length <= 0) diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 690e2b6f..8578ec4e 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -47,7 +47,6 @@ public class MediaUtils { private static OggClip mAudioClip = null; - /* contains dot! */ public static String extensionForMIME(String mimeType) { MimeType mime = null; try { @@ -55,7 +54,12 @@ public static String extensionForMIME(String mimeType) { } catch (MimeTypeException ex) { LOGGER.log(Level.WARNING, "can't find mimetype", ex); } - return StringUtils.defaultIfEmpty(mime != null ? mime.getExtension() : "", ".dat"); + + String m = mime != null ? mime.getExtension() : ""; + // remove dot + if (!m.isEmpty()) + m = m.substring(1); + return StringUtils.defaultIfEmpty(m, "dat"); } public enum Sound{NOTIFICATION} From d4a9325fcccdff3bc7404b515c173cc48ed11dce Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 12 Feb 2016 16:51:30 +0100 Subject: [PATCH 142/257] util: added method for resizing image by max pixels (unused) --- src/main/java/org/kontalk/model/Avatar.java | 2 +- .../java/org/kontalk/util/MediaUtils.java | 44 ++++++++++++++----- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 848dc0c2..d418330e 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -163,7 +163,7 @@ public static UserAvatar instance() { } public static UserAvatar setImage(BufferedImage image) { - image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE, true); + image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE); USER_AVATAR = new UserAvatar(image); return USER_AVATAR; } diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 8578ec4e..164f37ea 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -118,12 +118,16 @@ public static Optional readImage(byte[] imgData) { } public static boolean writeImage(BufferedImage img, String format, File output) { + boolean succ; try { - return ImageIO.write(img, format, output); + succ = ImageIO.write(img, format, output); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't save image", ex); return false; } + if (!succ) + LOGGER.warning("can't find writer for format: "+format); + return succ; } public static byte[] imageToByteArray(Image image, String format) { @@ -150,12 +154,28 @@ public static byte[] imageToByteArray(Image image, String format) { } /** - * Scale image down preserving ratio. + * Scale image down to max pixels preserving ratio. + * Blocking + */ + public static BufferedImage scale(BufferedImage image, int maxPixels) { + int iw = image.getWidth(); + int ih = image.getHeight(); + + double scale = Math.sqrt(maxPixels / (iw * ih * 1.0)); + System.out.println("iw= "+iw+" ih="+ih+" maxP="+maxPixels+" scale="+scale); + + return toBufferedImage(scaleAsync(image, (int) (iw * scale), (int) (ih * scale))); + } + + /** + * Scale image down to max width/height preserving ratio. * Blocking. */ - public static BufferedImage scale(Image image, int width, int height, boolean max) { - image = scaleAsync(image, width, height, max); + public static BufferedImage scale(Image image, int width, int height) { + return toBufferedImage(scaleAsync(image, width, height, true)); + } + private static BufferedImage toBufferedImage(Image image) { final CountDownLatch latch = new CountDownLatch(1); ImageObserver observer = new ImageObserver() { @@ -184,11 +204,7 @@ public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, in } } - return toBufferedImage(image); - } - - // source: https://stackoverflow.com/a/13605411 - private static BufferedImage toBufferedImage(Image image) { + // convert to buffered image, source: https://stackoverflow.com/a/13605411 if (image instanceof BufferedImage) return (BufferedImage) image; @@ -198,7 +214,7 @@ private static BufferedImage toBufferedImage(Image image) { LOGGER.warning("image not loaded yet"); } - BufferedImage bimage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB); + BufferedImage bimage = new BufferedImage(iw, ih, BufferedImage.TYPE_3BYTE_BGR); Graphics2D bGr = bimage.createGraphics(); bGr.drawImage(image, 0, 0, null); @@ -210,7 +226,7 @@ private static BufferedImage toBufferedImage(Image image) { /** * Scale image down to maximum or minimum of width or height, preserving ratio. * Async: returned image may not fully loaded. - * + * * @param max specifies if image is scaled to maximum or minimum of width/height */ public static Image scaleAsync(Image image, int width, int height, boolean max) { @@ -225,6 +241,10 @@ public static Image scaleAsync(Image image, int width, int height, boolean max) double sw = width / (iw * 1.0); double sh = height / (ih * 1.0); double scale = max ? Math.max(sw, sh) : Math.min(sw, sh); - return image.getScaledInstance((int) (iw * scale), (int) (ih * scale), Image.SCALE_FAST); + return scaleAsync(image, (int) (iw * scale), (int) (ih * scale)); + } + + private static Image scaleAsync(Image image, int width, int height) { + return image.getScaledInstance(width, height, Image.SCALE_FAST); } } From f6426d5839c2e5c1a450f6fe205ca9af0f01385a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 12 Feb 2016 17:01:49 +0100 Subject: [PATCH 143/257] control: resize image attachments before uploading --- src/main/java/org/kontalk/crypto/Coder.java | 4 +- .../java/org/kontalk/crypto/Decryptor.java | 2 +- .../java/org/kontalk/crypto/Encryptor.java | 11 +-- .../kontalk/model/message/MessageContent.java | 20 ++--- .../org/kontalk/model/message/OutMessage.java | 5 +- .../org/kontalk/system/AttachmentManager.java | 80 +++++++++++++------ src/main/java/org/kontalk/system/Config.java | 2 + src/main/java/org/kontalk/system/Control.java | 8 +- .../java/org/kontalk/view/MessageList.java | 2 +- 9 files changed, 80 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index 206207b3..5fb6c9bd 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -146,7 +146,7 @@ public static Optional encryptStanza(OutMessage message, String xml) { return new Encryptor(message).encryptStanza(xml); } - public static Optional encryptAttachment(OutMessage message) { - return new Encryptor(message).encryptAttachment(); + public static Optional encryptAttachment(OutMessage message, File file) { + return new Encryptor(message).encryptAttachment(file); } } diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 4702c944..0a184dd3 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -169,7 +169,7 @@ void decryptAttachment(Path baseDir) { } // in file - File inFile = baseDir.resolve(attachment.getFile()).toFile(); + File inFile = baseDir.resolve(attachment.getFilePath()).toFile(); // out file String base = FilenameUtils.getBaseName(inFile.getName()); String ext = FilenameUtils.getExtension(inFile.getName()); diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index b84c7888..9be4ff61 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -50,7 +50,6 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; import org.kontalk.model.Contact; -import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.OutMessage; import org.kontalk.model.message.Transmission; import org.kontalk.util.CPIMMessage; @@ -121,13 +120,7 @@ private Optional encryptData(String data, String mime) { return Optional.of(out.toByteArray()); } - Optional encryptAttachment() { - MessageContent.Attachment attachment = message.getContent().getAttachment().orElse(null); - if (attachment == null) { - LOGGER.warning("no attachment in out-message"); - return Optional.empty(); - } - + Optional encryptAttachment(File file) { boolean loaded = this.loadKeys(); if (!loaded) return Optional.empty(); @@ -140,7 +133,7 @@ Optional encryptAttachment() { return Optional.empty(); } - try (FileInputStream in = new FileInputStream(attachment.getFile().toFile()); + try (FileInputStream in = new FileInputStream(file); FileOutputStream out = new FileOutputStream(tempFile)) { encryptAndSign(in, out, myKey, receiverKeys); } catch (IOException | PGPException ex) { diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index 3bfbd83d..7075fc16 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -257,17 +257,17 @@ public static class Attachment { // file name of downloaded file or path to upload file, empty by default private Path mFile; // MIME of file, empty string by default - private final String mMimeType; - // size of (decrypted) file in bytes, -1 by default - private final long mLength; + private String mMimeType; + // size of (decrypted) upload file in bytes, -1 by default + private long mLength; // coder status of file encryption private final CoderStatus mCoderStatus; // progress downloaded of (encrypted) file in percent private int mDownloadProgress = -1; // used for outgoing attachments - public Attachment(Path path, String mimeType, long length) { - this(URI.create(""), path, mimeType, length, + public Attachment(Path path, String mimeType) { + this(URI.create(""), path, mimeType, -1, CoderStatus.createInsecure()); } @@ -299,8 +299,10 @@ public URI getURL() { return mURL; } - public void setURL(URI url){ + public void update(URI url, String mime, long length){ mURL = url; + mMimeType = mime; + mLength = length; } public String getMimeType() { @@ -314,7 +316,7 @@ public long getLength() { /** * Return the filename (download) or path to the local file (upload). */ - public Path getFile() { + public Path getFilePath() { return mFile; } @@ -342,7 +344,7 @@ public int getDownloadProgress() { return mDownloadProgress; } - /** Set download progress. See .getDownloadProgress() */ + /** Set download progress. See getDownloadProgress() */ void setDownloadProgress(int p) { mDownloadProgress = p; } @@ -350,7 +352,7 @@ void setDownloadProgress(int p) { @Override public String toString() { return "{ATT:url="+mURL+",file="+mFile+",mime="+mMimeType - +",status="+mCoderStatus+"}"; + +"length="+mLength+",status="+mCoderStatus+"}"; } // using legacy lib, raw types extend Object diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 07946e27..f6488d39 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -109,12 +109,13 @@ public void setServerError(String condition, String text) { this.setStatus(Status.ERROR); } - public void setAttachmentURL(URI url) { + /** Update attachment after upload. */ + public void setUpload(URI url, String mime, long length) { MessageContent.Attachment attachment = this.getAttachment(); if (attachment == null) return; - attachment.setURL(url); + attachment.update(url, mime, length); this.save(); } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 2435e19c..6ac3221e 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -27,7 +27,6 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -61,6 +60,7 @@ public class AttachmentManager implements Runnable { private static final String ATT_DIRNAME = "attachments"; private static final String PREVIEW_DIRNAME = "preview"; + private static final String RESIZED_IMG_MIME = "image/jpeg"; public static final Dimension THUMBNAIL_DIM = new Dimension(300, 200); public static final String THUMBNAIL_MIME = "image/jpeg"; @@ -80,7 +80,7 @@ public class AttachmentManager implements Runnable { "audio/wav"); // TODO get this from server - private static final String UPLOAD_URL = "https://beta.kontalk.net:5980/upload"; + private static final URI UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); private final LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); @@ -141,13 +141,49 @@ private void uploadAsync(OutMessage message) { return; } + File original; + File file = original = attachment.getFilePath().toFile(); + String mime = attachment.getMimeType(); + + if(isImage(attachment.getMimeType())) { + int maxImgSize = Config.getInstance().getInt(Config.NET_MAX_IMG_SIZE); + if (maxImgSize > 0) { + BufferedImage img = MediaUtils.readImage(file).orElse(null); + if (img == null) { + LOGGER.warning("can't load image"); + return; + } + if (img.getWidth() * img.getHeight() > maxImgSize) { + // image needs to be resized + BufferedImage resized = MediaUtils.scale(img, maxImgSize); + try { + file = File.createTempFile("kontalk_resized_img_att", ".dat"); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't create temporary file", ex); + return; + } + mime = RESIZED_IMG_MIME; + boolean succ = MediaUtils.writeImage(resized, + MediaUtils.extensionForMIME(mime), + file); + if (!succ) + return; + } + } + } + // if text will be encrypted, always encrypt attachment too boolean encrypt = message.getCoderStatus().getEncryption() == Encryption.DECRYPTED; - File file = encrypt ? - Coder.encryptAttachment(message).orElse(null) : - attachment.getFile().toFile(); - if (file == null) - return; + if (encrypt) { + File encryptFile = Coder.encryptAttachment(message, file).orElse(null); + if (!file.equals(original)) + file.delete(); + if (encryptFile == null) + return; + file = encryptFile; + // mime (and length) actually changed, but the server can't handle the truth + //mime = "application/octet-stream"; + } HTTPFileClient client = createClientOrNull(); if (client == null) @@ -155,10 +191,7 @@ private void uploadAsync(OutMessage message) { URI url; try { - url = client.upload(file, URI.create(UPLOAD_URL), - // this isn't correct, but the server can't handle the truth - /*encrypt ? "application/octet-stream" :*/ attachment.getMimeType(), - encrypt); + url = client.upload(file, UPLOAD_URI, mime, encrypt); } catch (KonException ex) { LOGGER.warning("upload failed, attachment: "+attachment); message.setStatus(KonMessage.Status.ERROR); @@ -166,8 +199,8 @@ private void uploadAsync(OutMessage message) { return; } - // delete temp file - if (encrypt) + long length = file.length(); + if (!file.equals(original)) file.delete(); if (url.toString().isEmpty()) { @@ -175,7 +208,7 @@ private void uploadAsync(OutMessage message) { return; } - message.setAttachmentURL(url); + message.setUpload(url, mime, length); LOGGER.info("upload successful, URL="+url); @@ -250,7 +283,7 @@ boolean createImagePreview(KonMessage message) { LOGGER.warning("no attachment in message: "+message); return false; } - Path path = filePath(att); + Path path = absoluteFilePath(att); if (!isImage(att.getMimeType())) return false; @@ -283,11 +316,11 @@ boolean createImagePreview(KonMessage message) { return true; } - Path filePath(Attachment attachment) { - Path path = attachment.getFile(); - if (path.toString().isEmpty()) - return Paths.get(""); - return path.isAbsolute() ? path : mAttachmentDir.resolve(path); + Path absoluteFilePath(Attachment attachment) { + Path path = attachment.getFilePath(); + return path.toString().isEmpty() || path.isAbsolute() ? + path : + mAttachmentDir.resolve(path); } Optional imagePreviewPath(KonMessage message) { @@ -363,12 +396,7 @@ static Attachment attachmentOrNull(Path path) { LOGGER.log(Level.WARNING, "can't get attachment mime type", ex); return null; } - long length = file.length(); - if (length <= 0) { - LOGGER.warning("invalid attachment file size: "+length); - return null; - } - return new Attachment(path, mimeType, length); + return new Attachment(path, mimeType); } private static HTTPFileClient createClientOrNull(){ diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index e0bcc36f..8583a39f 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -60,6 +60,7 @@ public final class Config extends PropertiesConfiguration { public static final String NET_STATUS_LIST = "net.status_list"; public static final String NET_AUTO_SUBSCRIPTION = "net.auto_subscription"; public static final String NET_REQUEST_AVATARS = "net.request_avatars"; + public static final String NET_MAX_IMG_SIZE = "net.max_img_size"; public static final String MAIN_CONNECT_STARTUP = "main.connect_startup"; public static final String MAIN_TRAY = "main.tray"; public static final String MAIN_TRAY_CLOSE = "main.tray_close"; @@ -104,6 +105,7 @@ private Config(Path configFile) { map.put(NET_STATUS_LIST, new String[]{DEFAULT_XMPP_STATUS}); map.put(NET_AUTO_SUBSCRIPTION, false); map.put(NET_REQUEST_AVATARS, true); + map.put(NET_MAX_IMG_SIZE, -1); map.put(MAIN_CONNECT_STARTUP, true); map.put(MAIN_TRAY, true); map.put(MAIN_TRAY_CLOSE, false); diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 2c5e4d31..61ba997e 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -608,7 +608,7 @@ public void setStatusText(String newStatus) { } public Path getFilePath(Attachment attachment) { - return mAttachmentManager.filePath(attachment); + return mAttachmentManager.absoluteFilePath(attachment); } public Optional getImagePath(KonMessage message) { @@ -743,11 +743,11 @@ public void downloadAgain(InMessage message) { } public void sendText(Chat chat, String text) { - this.sendTextMessage(chat, text, Paths.get("")); + this.sendNewMessage(chat, text, Paths.get("")); } public void sendAttachment(Chat chat, Path file){ - this.sendTextMessage(chat, "", file); + this.sendNewMessage(chat, "", file); } public void setUserAvatar(BufferedImage image) { @@ -770,7 +770,7 @@ public void unsetUserAvatar(){ /* private */ - private void sendTextMessage(Chat chat, String text, Path file) { + private void sendNewMessage(Chat chat, String text, Path file) { Attachment attachment = null; if (!file.toString().isEmpty()) { attachment = AttachmentManager.attachmentOrNull(file); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 806c9929..f6c9b195 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -202,7 +202,7 @@ public void actionPerformed(ActionEvent event) { } Attachment att = m.getContent().getAttachment().orElse(null); if (att != null && - att.getFile().toString().isEmpty()) { + att.getFilePath().toString().isEmpty()) { WebMenuItem attMenuItem = new WebMenuItem(Tr.tr("Load")); attMenuItem.setToolTipText(Tr.tr("Retry downloading attachment")); attMenuItem.addActionListener(new ActionListener() { From 433be99c3d68ea7639de53da8015b9dcfbb8015b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 12 Feb 2016 17:36:07 +0100 Subject: [PATCH 144/257] view: new network panel in configuration dialog with reduce image option, closes #53 --- .../org/kontalk/view/ConfigurationDialog.java | 82 ++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 5709c266..bc88e7ea 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -23,6 +23,7 @@ import com.alee.extended.panel.GroupingType; import com.alee.laf.button.WebButton; import com.alee.laf.checkbox.WebCheckBox; +import com.alee.laf.combobox.WebComboBox; import com.alee.laf.label.WebLabel; import com.alee.laf.panel.WebPanel; import com.alee.laf.rootpane.WebDialog; @@ -42,6 +43,8 @@ import java.awt.event.ItemListener; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Box; @@ -61,8 +64,6 @@ final class ConfigurationDialog extends WebDialog { private static final Logger LOGGER = Logger.getLogger(ConfigurationDialog.class.getName()); - private static enum ConfPage {MAIN, ACCOUNT}; - private final Config mConf = Config.getInstance(); private final View mView; @@ -71,7 +72,7 @@ private static enum ConfPage {MAIN, ACCOUNT}; mView = view; this.setTitle(Tr.tr("Preferences")); - this.setSize(550, 500); + this.setSize(550, 450); this.setResizable(false); this.setModal(true); this.setLayout(new BorderLayout(View.GAP_SMALL, View.GAP_SMALL)); @@ -79,9 +80,11 @@ private static enum ConfPage {MAIN, ACCOUNT}; WebTabbedPane tabbedPane = new WebTabbedPane(WebTabbedPane.LEFT); tabbedPane.setFontSize(View.FONT_SIZE_NORMAL); final MainPanel mainPanel = new MainPanel(); + final NetworkPanel networkPanel = new NetworkPanel(); final AccountPanel accountPanel = new AccountPanel(); final PrivacyPanel privacyPanel = new PrivacyPanel(); tabbedPane.addTab(Tr.tr("Main"), mainPanel); + tabbedPane.addTab(Tr.tr("Network"), networkPanel); tabbedPane.addTab(Tr.tr("Account"), accountPanel); tabbedPane.addTab(Tr.tr("Privacy"), privacyPanel); @@ -102,6 +105,8 @@ public void actionPerformed(ActionEvent e) { mainPanel.saveConfiguration(); accountPanel.saveConfiguration(); privacyPanel.saveConfiguration(); + networkPanel.saveConfiguration(); + ConfigurationDialog.this.dispose(); } }); @@ -112,12 +117,9 @@ public void actionPerformed(ActionEvent e) { } private class MainPanel extends WebPanel { - - private final WebCheckBox mConnectStartupBox; private final WebCheckBox mTrayBox; private final WebCheckBox mCloseTrayBox; private final WebCheckBox mEnterSendsBox; - private final WebCheckBox mRequestAvatars; private final WebCheckBox mUserContact; private final WebCheckBox mBGBox; private final WebFileChooserField mBGChooser; @@ -129,11 +131,6 @@ private class MainPanel extends WebPanel { groupPanel.add(new WebLabel(Tr.tr("Main Settings")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); - mConnectStartupBox = createCheckBox(Tr.tr("Connect on startup"), - "", - mConf.getBoolean(Config.MAIN_CONNECT_STARTUP)); - groupPanel.add(mConnectStartupBox); - mTrayBox = createCheckBox(Tr.tr("Show tray icon"), "", mConf.getBoolean(Config.MAIN_TRAY)); @@ -154,11 +151,6 @@ public void itemStateChanged(ItemEvent e) { mConf.getBoolean(Config.MAIN_ENTER_SENDS)); groupPanel.add(new GroupPanel(mEnterSendsBox, new WebSeparator())); - mRequestAvatars = createCheckBox(Tr.tr("Download profile pictures"), - Tr.tr("Download contact profile pictures"), - mConf.getBoolean(Config.NET_REQUEST_AVATARS)); - groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator())); - mUserContact = createCheckBox(Tr.tr("Show yourself in contacts"), Tr.tr("Show yourself in the contact list"), mConf.getBoolean(Config.VIEW_USER_CONTACT)); @@ -184,13 +176,11 @@ public void itemStateChanged(ItemEvent e) { } private void saveConfiguration() { - mConf.setProperty(Config.MAIN_CONNECT_STARTUP, mConnectStartupBox.isSelected()); mConf.setProperty(Config.MAIN_TRAY, mTrayBox.isSelected()); mConf.setProperty(Config.MAIN_TRAY_CLOSE, mCloseTrayBox.isSelected()); mView.updateTray(); mConf.setProperty(Config.MAIN_ENTER_SENDS, mEnterSendsBox.isSelected()); mView.setHotkeys(); - mConf.setProperty(Config.NET_REQUEST_AVATARS, mRequestAvatars.isSelected()); mConf.setProperty(Config.VIEW_USER_CONTACT, mUserContact.isSelected()); mView.updateContactList(); String bgPath; @@ -207,6 +197,60 @@ private void saveConfiguration() { } } + private class NetworkPanel extends WebPanel { + + private final WebCheckBox mConnectStartupBox; + private final WebCheckBox mRequestAvatars; + private final WebComboBox mMaxImgSizeBox; + private final LinkedHashMap mImgResizeMap; + + public NetworkPanel() { + GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); + groupPanel.setMargin(View.MARGIN_BIG); + + groupPanel.add(new WebLabel(Tr.tr("Network Settings")).setBoldFont()); + groupPanel.add(new WebSeparator(true, true)); + + mConnectStartupBox = createCheckBox(Tr.tr("Connect on startup"), + "", + mConf.getBoolean(Config.MAIN_CONNECT_STARTUP)); + groupPanel.add(mConnectStartupBox); + + mRequestAvatars = createCheckBox(Tr.tr("Download profile pictures"), + Tr.tr("Download contact profile pictures"), + mConf.getBoolean(Config.NET_REQUEST_AVATARS)); + groupPanel.add(new GroupPanel(mRequestAvatars, new WebSeparator())); + + mImgResizeMap = new LinkedHashMap<>(); + mImgResizeMap.put(-1, Tr.tr("Original")); + mImgResizeMap.put(300 * 1000, Tr.tr("Small (0.3MP)")); + mImgResizeMap.put(500 * 1000, Tr.tr("Medium (0.5MP)")); + mImgResizeMap.put(800 * 1000, Tr.tr("Large (0.8MP)")); + + mMaxImgSizeBox = new WebComboBox(new ArrayList<>(mImgResizeMap.values()).toArray()); + int maxImgSize = mConf.getInt(Config.NET_MAX_IMG_SIZE); + int maxImgIndex = new ArrayList<>(mImgResizeMap.keySet()).indexOf(maxImgSize); + if (maxImgSize >= 0) + mMaxImgSizeBox.setSelectedIndex(maxImgIndex); + TooltipManager.addTooltip(mMaxImgSizeBox, Tr.tr("Reduce size of images before sending")); + + groupPanel.add(new GroupPanel(View.GAP_DEFAULT, + new WebLabel(Tr.tr("Resize image attachments:")), + mMaxImgSizeBox, + new WebSeparator())); + + this.add(groupPanel); + } + + private void saveConfiguration() { + mConf.setProperty(Config.MAIN_CONNECT_STARTUP, mConnectStartupBox.isSelected()); + mConf.setProperty(Config.NET_REQUEST_AVATARS, mRequestAvatars.isSelected()); + + mConf.setProperty(Config.NET_MAX_IMG_SIZE, + new ArrayList<>(mImgResizeMap.keySet()).get(mMaxImgSizeBox.getSelectedIndex())); + } + } + private class AccountPanel extends WebPanel { private final WebTextField mServerField; @@ -305,7 +349,7 @@ private void updateKey() { PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); String uid = key != null ? key.getUserId() : null; mUserIDArea.setText(uid != null ? - StringUtils.abbreviate(uid, 40) : + StringUtils.abbreviate(uid, 30) : "- "+Tr.tr("no key loaded")+" -"); if (uid != null) TooltipManager.addTooltip(mUserIDArea, uid); From fff0d745efcf78cacabe880c80942bbde239d75c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 12 Feb 2016 17:38:39 +0100 Subject: [PATCH 145/257] i18n: (auto)update translation strings --- src/main/resources/i18n/strings.properties | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index ab3b203a..58e9f6dd 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -248,3 +248,12 @@ s_T2TO = Show yourself in contacts s_F1X0 = Show yourself in the contact list s_3PFR = Remove s_D7FH = Not supported by server +s_WUDV = Group Owner +s_RCDY = Network +s_LPE1 = Network Settings +s_R4F8 = Original +s_K83J = Small (0.3MP) +s_RAMV = Medium (0.5MP) +s_7CPG = Large (0.8MP) +s_5RTB = Reduce size of images before sending +s_13FX = Resize image attachments: From 9886222530f4715330ee2ea620dab017e1ae7318 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 16 Feb 2016 20:12:23 +0100 Subject: [PATCH 146/257] upgrade to Java 8 --- build.gradle | 4 +- src/main/java/org/kontalk/Kontalk.java | 1 + .../java/org/kontalk/model/ContactList.java | 10 ++-- .../java/org/kontalk/model/chat/Chat.java | 20 +++----- .../java/org/kontalk/model/chat/ChatList.java | 3 -- .../org/kontalk/model/chat/ChatMessages.java | 16 +++---- .../org/kontalk/model/chat/GroupChat.java | 48 ++++++------------- .../java/org/kontalk/model/chat/Member.java | 1 - .../kontalk/model/message/MessageContent.java | 13 ++--- .../kontalk/model/message/ProtoMessage.java | 3 +- .../org/kontalk/system/ChatStateManager.java | 4 +- src/main/java/org/kontalk/system/Control.java | 8 ++-- .../java/org/kontalk/system/Database.java | 7 +-- 13 files changed, 51 insertions(+), 87 deletions(-) diff --git a/build.gradle b/build.gradle index aea514ae..833b8279 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ apply plugin: 'application' apply plugin: 'java' -sourceCompatibility = '1.7' -targetCompatibility = '1.7' +sourceCompatibility = '1.8' +targetCompatibility = '1.8' mainClassName = 'org.kontalk.Kontalk' ext.clientCommonDir = 'client-common-java' diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index d53ea787..05f6c0c0 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -104,6 +104,7 @@ int start(boolean ui) { // check java version String jVersion = System.getProperty("java.version"); if (jVersion.startsWith("1.7")) { + // TODO this won't happen anymore View.showWrongJavaVersionDialog(); LOGGER.severe("java too old: "+jVersion); return 3; diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 414d65e1..07623375 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -28,7 +28,6 @@ import java.util.Observable; import java.util.Optional; import java.util.Set; -import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -128,12 +127,9 @@ public Optional getMe() { } public Set getAll(final boolean withMe) { - return new HashSet<>(mJIDMap.values().stream().filter(new Predicate(){ - @Override - public boolean test(Contact t) { - return withMe || !t.isMe(); - } - }).collect(Collectors.toSet())); + return mJIDMap.values().stream() + .filter(c -> (withMe || !c.isMe())) + .collect(Collectors.toCollection(HashSet::new)); } public void delete(Contact contact) { diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 99874267..4bceb816 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -215,17 +215,13 @@ protected void save(List members, String subject) { List oldMembers = new ArrayList<>(this.getAllMembers()); // save new members - for (Member m : members) { - if (!oldMembers.contains(m)) { - m.insert(mID); - } - oldMembers.remove(m); - } + members.stream() + .filter(m -> !oldMembers.contains(m)) + .forEach(m -> m.insert(mID)); + oldMembers.removeAll(members); // whats left is too much and can be deleted - for (Member m : oldMembers) { - m.delete(); - } + oldMembers.stream().forEach(m -> m.delete()); } void delete() { @@ -246,10 +242,8 @@ void delete() { return; // members - boolean allDeleted = true; - for (Member member : this.getAllMembers()) { - allDeleted &= member.delete(); - } + boolean allDeleted = this.getAllMembers().stream() + .allMatch(m -> m.delete()); if (!allDeleted) return; diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index b551f737..6864f72f 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -18,9 +18,6 @@ package org.kontalk.model.chat; -import org.kontalk.model.chat.Chat; -import org.kontalk.model.chat.GroupChat; -import org.kontalk.model.chat.SingleChat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 23eaf0d0..ff554fc5 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -28,6 +28,7 @@ import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.OutMessage; import org.kontalk.system.Database; @@ -105,16 +106,11 @@ public NavigableSet getAll() { public SortedSet getPending() { this.ensureLoaded(); - SortedSet s = new TreeSet<>(); - // TODO performance, probably additional map needed - // TODO use lambda in near future - for (KonMessage m : mSet) { - if (m.getStatus() == KonMessage.Status.PENDING && - m instanceof OutMessage) { - s.add((OutMessage) m); - } - } - return s; + return mSet.stream() + .filter(m -> m.getStatus() == KonMessage.Status.PENDING + && m instanceof OutMessage) + .map(m -> (OutMessage) m) + .collect(Collectors.toCollection(TreeSet::new)); } /** Get the newest (ie last received) outgoing message. */ diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 1c4d21a0..6ad02576 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -22,9 +22,8 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; -import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.misc.JID; import org.kontalk.model.Contact; @@ -55,8 +54,7 @@ private GroupChat(List members, D gData, String subject) { mGroupData = gData; mSubject = subject; - for (Member member: members) - this.addMemberSilent(member); + members.stream().forEach(m -> this.addMemberSilent(m)); } // used when loading from database @@ -72,8 +70,7 @@ private GroupChat(int id, mGroupData = gData; mSubject = subject; - for (Member member: members) - this.addMemberSilent(member); + members.stream().forEach(m -> this.addMemberSilent(m)); } @Override @@ -84,24 +81,17 @@ public List getAllMembers() { /** Get all contacts (including deleted and user contact). */ @Override public List getAllContacts() { - List l = new ArrayList<>(mMemberSet.size()); - for (Member m : mMemberSet) - l.add(m.getContact()); - - return l; + return mMemberSet.stream() + .map(m -> m.getContact()) + .collect(Collectors.toList()); } @Override public Contact[] getValidContacts() { - //chat.getContacts().stream().filter(c -> !c.isDeleted()); - Set contacts = new HashSet<>(); - for (Member m : mMemberSet) { - Contact c = m.getContact(); - if (!c.isDeleted() && !c.isMe()) { - contacts.add(m.getContact()); - } - } - return contacts.toArray(new Contact[0]); + return mMemberSet.stream() + .map(m -> m.getContact()) + .filter(c -> (!c.isDeleted() && !c.isMe())) + .toArray(Contact[]::new); } public void addContact(Contact contact) { @@ -169,12 +159,9 @@ public void setSubject(String subject) { @Override public void setChatState(final Contact contact, ChatState chatState) { - Member member = mMemberSet.stream().filter(new Predicate(){ - @Override - public boolean test(Member t) { - return t.getContact().equals(contact); - } - }).findFirst().orElse(null); + Member member = mMemberSet.stream() + .filter(m -> m.getContact().equals(contact)) + .findFirst().orElse(null); if (member == null) { LOGGER.warning("can't find member in member set!?"); @@ -281,14 +268,7 @@ public boolean isAdministratable() { } private boolean containsMe() { - return mMemberSet.parallelStream().anyMatch( - new Predicate() { - @Override - public boolean test(Member t) { - return t.getContact().isMe(); - } - } - ); + return mMemberSet.stream().anyMatch(m -> m.getContact().isMe()); } @Override diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index 238b5576..258acdd4 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -18,7 +18,6 @@ package org.kontalk.model.chat; -import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index 7075fc16..ce86047e 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.crypto.Coder; @@ -566,14 +567,14 @@ private static GroupCommand fromJSONOrNull(String json) { String subj = EncodingUtils.getJSONString(map, JSON_SUBJECT); List a = (List) map.get(JSON_ADDED); - List added = new ArrayList<>(a.size()); - for (String s: a) - added.add(JID.bare(s)); + List added = a.stream() + .map(s -> JID.bare(s)) + .collect(Collectors.toList()); List r = (List) map.get(JSON_REMOVED); - List removed = new ArrayList<>(r.size()); - for (String s: r) - removed.add(JID.bare(s)); + List removed = r.stream() + .map(s -> JID.bare(s)) + .collect(Collectors.toList()); return new GroupCommand(op, added.toArray(new JID[0]), removed.toArray(new JID[0]), diff --git a/src/main/java/org/kontalk/model/message/ProtoMessage.java b/src/main/java/org/kontalk/model/message/ProtoMessage.java index 6729a0bb..1ede1507 100644 --- a/src/main/java/org/kontalk/model/message/ProtoMessage.java +++ b/src/main/java/org/kontalk/model/message/ProtoMessage.java @@ -31,8 +31,7 @@ public final class ProtoMessage implements DecryptMessage { private final Contact mContact; private final CoderStatus mCoderStatus; - - private MessageContent mContent; + private final MessageContent mContent; public ProtoMessage(Contact contact, MessageContent content) { mContact = contact; diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index 61f049e6..155c4963 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -57,8 +57,8 @@ void handleOwnChatStateEvent(Chat chat, ChatState state) { } void imGone() { - for (MyChatState chatState : mChatStateCache.values()) - chatState.handleState(ChatState.gone); + mChatStateCache.values().stream() + .forEach(chatState -> chatState.handleState(ChatState.gone)); } private class MyChatState { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 61ba997e..6e5ec5a5 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -32,6 +32,7 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.Kontalk; @@ -686,10 +687,9 @@ public Chat getOrCreateSingleChat(Contact contact) { public void createGroupChat(List contacts, String subject) { // user is part of the group - List members = new ArrayList<>(contacts.size()+1); - for (Contact c : contacts) { - members.add(new Member(c)); - } + List members = contacts.stream() + .map(c -> new Member(c)) + .collect(Collectors.toCollection(ArrayList::new)); Contact me = ContactList.getInstance().getMe().orElse(null); if (me == null) { LOGGER.warning("can't find myself"); diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index aeba97cb..fffa0e43 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -34,6 +34,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.kontalk.Kontalk; import org.kontalk.misc.JID; @@ -270,9 +271,9 @@ public synchronized int execUpdate(String table, Map set, int id List keyList = new ArrayList<>(set.keySet()); - List vList = new ArrayList<>(keyList.size()); - for (String key : keyList) - vList.add(key + " = ?"); + List vList = keyList.stream() + .map(key -> key + " = ?") + .collect(Collectors.toCollection(ArrayList::new)); update += StringUtils.join(vList, ", ") + " WHERE _id == " + id ; // note: looks like driver doesn't support "LIMIT" From 16d7f2e15d6cc250cdd3c0aa8f3827856bdb1735 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 16 Feb 2016 20:14:33 +0100 Subject: [PATCH 147/257] crypto: minor simplification --- src/main/java/org/kontalk/crypto/PersonalKey.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/PersonalKey.java b/src/main/java/org/kontalk/crypto/PersonalKey.java index 72f1bf17..ec89941c 100644 --- a/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -39,7 +39,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.util.encoders.Hex; @@ -159,10 +158,7 @@ public static PersonalKey load(byte[] privateKeyData, } // decrypt private keys - PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder( - // TODO need this? - new JcaPGPDigestCalculatorProviderBuilder().build() - ) + PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder() .setProvider(PGPUtils.PROVIDER) .build(passphrase); PGPKeyPair authKeyPair = PGPUtils.decrypt(authKey, decryptor); From 534d53643339f0d3101e0e8882a03ff3b4e9e63d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 17 Feb 2016 17:05:33 +0100 Subject: [PATCH 148/257] model: replaced arrays with list; more usage of streams --- .../kontalk/model/message/MessageContent.java | 50 ++++++++++--------- src/main/java/org/kontalk/system/Control.java | 9 +--- .../java/org/kontalk/system/GroupControl.java | 23 +++++---- .../java/org/kontalk/util/ClientUtils.java | 13 +++-- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index ce86047e..e3ca9897 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -22,7 +22,7 @@ import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -492,29 +492,33 @@ public enum OP { } private final OP mOP; - private final JID[] mJIDsAdded; - private final JID[] mJIDsRemoved; + private final List mAdded; + private final List mRemoved; private final String mSubject; /** Group creation. */ - public static GroupCommand create(JID[] added, String subject) { - return new GroupCommand(OP.CREATE, added, new JID[0], subject); + public static GroupCommand create(List added, String subject) { + return new GroupCommand(OP.CREATE, added, Collections.emptyList(), subject); } /** Group changed. */ - public static GroupCommand set(JID[] added, JID[] removed, String subject) { + public static GroupCommand set(List added, List removed, String subject) { return new GroupCommand(OP.SET, added, removed, subject); } + public static GroupCommand set(String subject) { + return new GroupCommand(OP.SET, Collections.emptyList(), Collections.emptyList(), subject); + } + /** Member left. Identified by sender JID */ public static GroupCommand leave() { - return new GroupCommand(OP.LEAVE, new JID[0], new JID[0], ""); + return new GroupCommand(OP.LEAVE, Collections.emptyList(), Collections.emptyList(), ""); } - private GroupCommand(OP operation, JID[] added, JID[] removed, String subject) { + private GroupCommand(OP operation, List added, List removed, String subject) { mOP = operation; - mJIDsAdded = added; - mJIDsRemoved = removed; + mAdded = added; + mRemoved = removed; mSubject = subject; } @@ -522,12 +526,12 @@ public OP getOperation() { return mOP; } - public JID[] getAdded() { - return mJIDsAdded; + public List getAdded() { + return mAdded; } - public JID[] getRemoved() { - return mJIDsRemoved; + public List getRemoved() { + return mRemoved; } public String getSubject() { @@ -541,14 +545,14 @@ private String toJSON() { json.put(JSON_OP, mOP.ordinal()); EncodingUtils.putJSON(json, JSON_SUBJECT, mSubject); - List added = new ArrayList(mJIDsAdded.length); - for (JID jid: mJIDsAdded) - added.add(jid.string()); + List added = mAdded.stream() + .map(jid -> jid.string()) + .collect(Collectors.toList()); json.put(JSON_ADDED, added); - List removed = new ArrayList(mJIDsRemoved.length); - for (JID jid: mJIDsAdded) - removed.add(jid.string()); + List removed = mAdded.stream() + .map(jid -> jid.string()) + .collect(Collectors.toList()); json.put(JSON_REMOVED, removed); return json.toJSONString(); @@ -576,10 +580,8 @@ private static GroupCommand fromJSONOrNull(String json) { .map(s -> JID.bare(s)) .collect(Collectors.toList()); - return new GroupCommand(op, - added.toArray(new JID[0]), removed.toArray(new JID[0]), - subj); - } catch (NullPointerException | ClassCastException ex) { + return new GroupCommand(op, added, removed, subj); + } catch (NullPointerException | ClassCastException ex) { LOGGER.log(Level.WARNING, "can't parse JSON group command", ex); LOGGER.log(Level.WARNING, "JSON='"+json+"'"); return null; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 6e5ec5a5..0b65bcd4 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -718,14 +718,7 @@ public void deleteChat(Chat chat) { } public void setChatSubject(GroupChat chat, String subject) { - if (!chat.isAdministratable()) { - LOGGER.warning("not admin"); - return; - } - Control.this.createAndSendMessage(chat, MessageContent.groupCommand( - GroupCommand.set(new JID[0], new JID[0], subject))); - - chat.setSubject(subject); + mGroupControl.getInstanceFor(chat).onSetSubject(subject); } public void handleOwnChatStateEvent(Chat chat, ChatState state) { diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 47eae986..52f9850d 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -82,7 +82,7 @@ void onCreate() { mControl.createAndSendMessage(mChat, MessageContent.groupCommand( MessageContent.GroupCommand.create( - jids.toArray(new JID[0]), + jids, mChat.getSubject()) ) ); @@ -90,8 +90,15 @@ void onCreate() { @Override public void onSetSubject(String subject) { + if (!mChat.isAdministratable()) { + LOGGER.warning("not admin"); + return; + } + mControl.createAndSendMessage(mChat, MessageContent.groupCommand( - MessageContent.GroupCommand.set(new JID[0], new JID[0], subject))); + GroupCommand.set(subject))); + + mChat.setSubject(subject); } @Override @@ -127,11 +134,11 @@ public void onInMessage(GroupCommand command, Contact sender) { // add contacts if necessary // TODO design problem here: we need at least the public keys, but user // might dont wanna have group members in contact list - for (JID jid : command.getAdded()) { + command.getAdded().stream().forEach(jid -> { boolean succ = mControl.getOrCreateContact(jid).isPresent(); if (!succ) LOGGER.warning("can't create contact, JID: "+jid); - } + }); } mChat.applyGroupCommand(command, sender); @@ -139,11 +146,9 @@ public void onInMessage(GroupCommand command, Contact sender) { } ChatControl getInstanceFor(GroupChat chat) { - // TODO - return (chat instanceof KonGroupChat) ? - new KonChatControl((KonGroupChat) chat) : - // new MUCControl((MUCChat) chat); - null; + if (chat instanceof KonGroupChat) + return new KonChatControl((KonGroupChat) chat); + throw new IllegalArgumentException("Not implemented for "+chat); } static KonGroupData newKonGroupData(JID myJID) { diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index a30a1b33..4fb4dacc 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -19,6 +19,7 @@ package org.kontalk.util; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -103,8 +104,8 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, String subject = groupCommand.getSubject(); if (op == OP.CREATE) { command = Type.CREATE; - for (JID added : groupCommand.getAdded()) - members.add(new Member(added.string())); + groupCommand.getAdded().stream().forEach(added -> + members.add(new Member(added.string()))); } else { command = Type.SET; Set incl = new HashSet<>(); @@ -116,7 +117,7 @@ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, incl.add(removed); members.add(new Member(removed.string(), Member.Operation.REMOVE)); } - if (groupCommand.getAdded().length > 0) { + if (!groupCommand.getAdded().isEmpty()) { // list all remaining member for the new member for (Contact c : chat.getValidContacts()) { JID old = c.getJID(); @@ -147,12 +148,14 @@ public static Optional groupExtensionToGroupCommand( List jids = new ArrayList<>(members.size()); for (Member m: members) jids.add(JID.bare(m.jid)); - return Optional.of(GroupCommand.create(jids.toArray(new JID[0]), subject)); + return Optional.of(GroupCommand.create(jids, subject)); case PART: return Optional.of(GroupCommand.leave()); case SET: // TODO - return Optional.of(GroupCommand.set(new JID[0], new JID[0], subject)); + return Optional.of(GroupCommand.set( + Collections.emptyList(), + Collections.emptyList(), subject)); case GET: case RESULT: default: From 02dba65820c80114ba3ce9813ee0bd39895e51d4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 18 Feb 2016 19:58:14 +0100 Subject: [PATCH 149/257] removed obsolete TODOs --- src/main/java/org/kontalk/Kontalk.java | 3 ++- src/main/java/org/kontalk/client/KonMessageSender.java | 2 +- src/main/java/org/kontalk/model/chat/GroupMetaData.java | 2 -- src/main/java/org/kontalk/system/ChatStateManager.java | 4 ++-- src/main/java/org/kontalk/system/Control.java | 3 ++- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 05f6c0c0..3ee18bda 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -104,7 +104,8 @@ int start(boolean ui) { // check java version String jVersion = System.getProperty("java.version"); if (jVersion.startsWith("1.7")) { - // TODO this won't happen anymore + // NOTE: we wont be here if 7 is used; still here for the bright + // future View.showWrongJavaVersionDialog(); LOGGER.severe("java too old: "+jVersion); return 3; diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index ef33269e..b45b10f6 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -87,7 +87,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { // extensions - // TODO with group chat? (for muc "NOT RECOMMENDED") + // not with group chat (at least not for Kontalk groups or MUC) if (!chat.isGroupChat()) protoMessage.addExtension(new DeliveryReceiptRequest()); diff --git a/src/main/java/org/kontalk/model/chat/GroupMetaData.java b/src/main/java/org/kontalk/model/chat/GroupMetaData.java index 2a54dbae..599e8e58 100644 --- a/src/main/java/org/kontalk/model/chat/GroupMetaData.java +++ b/src/main/java/org/kontalk/model/chat/GroupMetaData.java @@ -29,8 +29,6 @@ /** * Immutable meta data fields for a specific group chat protocol implementation. * - * TODO toString() methods - * * @author Alexander Bikadorov {@literal } */ public abstract class GroupMetaData { diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index 155c4963..b3b8f455 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -82,8 +82,8 @@ private void handleState(ChatState state) { mScheduledStateSet = new TimerTask() { @Override public void run() { - // TODO use 'inactive' instead of 'paused' for now as - // 'inactive' currently wont be send at all + // NOTE: using 'inactive' instead of 'paused' here as + // 'inactive' isn't send at all MyChatState.this.handleState(ChatState.inactive); } }; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 0b65bcd4..c4193b33 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -265,9 +265,10 @@ public void onChatStateNotification(MessageIDs ids, LOGGER.info("can't find contact with jid: "+ids.jid); return; } - // TODO chat states for group chats? + // NOTE: assume chat states are only send for single chats SingleChat chat = ChatList.getInstance().get(contact, ids.xmppThreadID).orElse(null); if (chat == null) + // not that important return; chat.setChatState(contact, chatState); From 5f22392fd3ea6fb656a4c4acecde6bd579ae3426 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 17:20:51 +0100 Subject: [PATCH 150/257] client: added server feature detection for Kontalk upload service --- src/main/java/org/kontalk/client/Client.java | 17 +++++++++++------ .../java/org/kontalk/client/HTTPFileClient.java | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 7139b7b5..8292d073 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -24,6 +24,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; @@ -68,21 +69,27 @@ public final class Client implements StanzaListener, Runnable { private static final String CAPS_CACHE_DIR = "caps_cache"; private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); + private static final Map FEATURE_MAP; public enum PresenceCommand {REQUEST, GRANT, DENY}; - public enum ServerFeature {USER_AVATAR} + public enum ServerFeature {USER_AVATAR, ATTACHMENT_UPLOAD} private enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; - private final HashMap mFeatureMap; private final EnumSet mFeatures; private KonConnection mConn = null; private AvatarSendReceiver mAvatarSendReceiver = null; + static { + FEATURE_MAP = new HashMap<>(); + FEATURE_MAP.put(PubSub.NAMESPACE, ServerFeature.USER_AVATAR); + FEATURE_MAP.put(HTTPFileClient.KON_UPLOAD_FEATURE, ServerFeature.ATTACHMENT_UPLOAD); + } + private Client(Control control) { mControl = control; //mLimited = limited; @@ -92,8 +99,6 @@ private Client(Control control) { // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; - mFeatureMap = new HashMap<>(); - mFeatureMap.put(PubSub.NAMESPACE, ServerFeature.USER_AVATAR); mFeatures = EnumSet.noneOf(ServerFeature.class); // setting caps cache @@ -231,8 +236,8 @@ private void connectAsync() { if (info != null) { for (DiscoverInfo.Feature feature: info.getFeatures()) { String var = feature.getVar(); - if (mFeatureMap.containsKey(var)) { - mFeatures.add(mFeatureMap.get(feature)); + if (FEATURE_MAP.containsKey(var)) { + mFeatures.add(FEATURE_MAP.get(var)); } } } diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index b197b6e6..9d017880 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -65,6 +65,8 @@ public class HTTPFileClient { private static final Logger LOGGER = Logger.getLogger(HTTPFileClient.class.getName()); + static final String KON_UPLOAD_FEATURE = "http://kontalk.org/extensions/message#upload"; + /** Regex used to parse content-disposition headers for download. */ private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); From 6188236b15ca089992c945c1cb3f42bc7ee76a60 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 18:43:11 +0100 Subject: [PATCH 151/257] view: indicate file upload server support by chat views file button --- src/main/java/org/kontalk/view/ChatView.java | 55 ++++++++++++++----- .../org/kontalk/view/ContactListView.java | 2 +- src/main/java/org/kontalk/view/MainFrame.java | 2 +- src/main/java/org/kontalk/view/Notifier.java | 2 +- .../java/org/kontalk/view/ProfileDialog.java | 2 +- src/main/java/org/kontalk/view/View.java | 12 ++-- 6 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 06b25267..55b174d7 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -56,6 +56,7 @@ import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.io.File; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -71,17 +72,19 @@ import org.apache.commons.io.FileUtils; import org.apache.tika.Tika; import org.jivesoftware.smackx.chatstates.ChatState; +import org.kontalk.client.Client; import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; import org.kontalk.system.Config; +import org.kontalk.system.Control; import org.kontalk.util.EncodingUtils; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; import static org.kontalk.view.View.MARGIN_SMALL; /** - * Pane that shows the currently selected chat. + * Panel showing the currently selected chat. * @author Alexander Bikadorov {@literal } */ final class ChatView extends WebPanel implements Observer { @@ -97,7 +100,8 @@ final class ChatView extends WebPanel implements Observer { private final WebTextArea mSendTextArea; private final WebLabel mEncryptionStatus; private final WebButton mSendButton; - private final WebFileChooser fileChooser; + private final WebFileChooser mFileChooser; + private final WebButton mFileButton; private final Map mChatCache = new HashMap<>(); @@ -210,10 +214,10 @@ public void actionPerformed(ActionEvent e) { } }); // file chooser button - fileChooser = new WebFileChooser(); - fileChooser.setMultiSelectionEnabled(false); - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - fileChooser.setFileFilter(new CustomFileFilter(AllFilesFilter.ICON, + mFileChooser = new WebFileChooser(); + mFileChooser.setMultiSelectionEnabled(false); + mFileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + mFileChooser.setFileFilter(new CustomFileFilter(AllFilesFilter.ICON, Tr.tr("Supported files")) { @Override public boolean accept(File file) { @@ -223,14 +227,12 @@ public boolean accept(File file) { } }); // mAttField.setPreferredWidth(150); - WebButton fileButton = new WebButton(Tr.tr("File"), Utils.getIcon("ic_ui_attach.png")) + mFileButton = new WebButton(Tr.tr("File"), Utils.getIcon("ic_ui_attach.png")) .setRound(0) .setBottomBgColor(titlePanel.getBackground()) .setMargin(1, MARGIN_SMALL, 1, MARGIN_SMALL); - TooltipManager.addTooltip(fileButton, - Tr.tr("Send File - max. size:") + " " + - FileUtils.byteCountToDisplaySize(AttachmentManager.MAX_ATT_SIZE)); - fileButton.addActionListener(new ActionListener() { + + mFileButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { ChatView.this.showFileDialog(); @@ -240,7 +242,7 @@ public void actionPerformed(ActionEvent e) { mEncryptionStatus = new WebLabel(); WebPanel textBarPanel = new GroupPanel(GroupingType.fillMiddle, 0, - fileButton, Box.createGlue(), new GroupPanel(View.GAP_DEFAULT, + mFileButton, Box.createGlue(), new GroupPanel(View.GAP_DEFAULT, mEncryptionStatus, mSendButton)) .setUndecorated(false) .setWebColoredBackground(false) @@ -369,6 +371,29 @@ public void keyTyped(KeyEvent e) { mSendButton.addHotkey(sendHotkey, TooltipWay.up); } + void onStatusChange(Control.Status status, EnumSet serverFeature) { + Boolean supported = null; + switch(status) { + case CONNECTED: + this.setColor(Color.WHITE); + supported = serverFeature.contains(Client.ServerFeature.ATTACHMENT_UPLOAD); + break; + case DISCONNECTED: + case ERROR: + this.setColor(Color.LIGHT_GRAY); + // don't know, but assume it + supported = true; + break; + } + if (supported != null) { + TooltipManager.setTooltip(mFileButton, Tr.tr("Send File") + " - " + (supported ? + Tr.tr("max. size:") + " " + + FileUtils.byteCountToDisplaySize(AttachmentManager.MAX_ATT_SIZE) : + mView.tr_not_supported)); + mFileButton.setForeground(supported ? Color.BLACK : Color.RED); + } + } + @Override public void update(Observable o, final Object arg) { if (SwingUtilities.isEventDispatchThread()) { @@ -497,11 +522,11 @@ private void sendMsg() { } private void showFileDialog() { - if (fileChooser.showOpenDialog(ChatView.this) != WebFileChooser.APPROVE_OPTION) + if (mFileChooser.showOpenDialog(ChatView.this) != WebFileChooser.APPROVE_OPTION) return; - File file = fileChooser.getSelectedFile(); - fileChooser.setCurrentDirectory(file.toPath().getParent().toString()); + File file = mFileChooser.getSelectedFile(); + mFileChooser.setCurrentDirectory(file.toPath().getParent().toString()); Chat chat = this.getCurrentChat().orElse(null); if (chat == null) diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index ae084bb0..dc9c04d8 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -138,7 +138,7 @@ public void actionPerformed(ActionEvent event) { @Override public void actionPerformed(ActionEvent event) { String text = Tr.tr("Permanently delete this contact?") + "\n" + - View.REMOVE_CONTACT_NOTE; + mView.tr_remove_contact; if (!Utils.confirmDeletion(ContactListView.this, text)) return; mView.getControl().deleteContact( diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 00dffba1..955e3a1a 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -74,7 +74,7 @@ static enum Tab {CHATS, CONTACT}; private final WebToggleButton mAddGroupButton; private final WebToggleButton mAddContactButton; - MainFrame(final View view, + MainFrame(View view, ListView contactList, ListView chatList, Component content, diff --git a/src/main/java/org/kontalk/view/Notifier.java b/src/main/java/org/kontalk/view/Notifier.java index b0c5fcc9..995f031e 100644 --- a/src/main/java/org/kontalk/view/Notifier.java +++ b/src/main/java/org/kontalk/view/Notifier.java @@ -164,7 +164,7 @@ void confirmContactDeletion(final Contact contact) { WebPanel panel = panel(Tr.tr("Contact was deleted on server"), contact); String expl = Tr.tr("Remove this contact from your contact list?") + "\n" + - View.REMOVE_CONTACT_NOTE; + mView.tr_remove_contact; panel.add(textArea(expl)); WebNotificationPopup popup = NotificationManager.showNotification(panel, diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 62e0a3ae..f0959e30 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -125,7 +125,7 @@ public void mouseClicked(MouseEvent e) { TooltipManager.addTooltip(mAvatarImage, mView.currentStatus() != Control.Status.CONNECTED ? Tr.tr("Not connected") : - Tr.tr("Not supported by server")); + mView.tr_not_supported); groupPanel.add(mAvatarImage); groupPanel.add(new WebSeparator(true, true)); diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index dc50f6fd..14105ef4 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -91,8 +91,6 @@ public final class View implements Observer { static final Dimension AVATAR_LIST_DIM = new Dimension(30, 30); - static final String REMOVE_CONTACT_NOTE = Tr.tr("Chats and messages will not be deleted."); - private final ViewControl mControl; private final TrayManager mTrayManager; @@ -106,6 +104,9 @@ public final class View implements Observer { private final WebStatusLabel mStatusBarLabel; private final MainFrame mMainFrame; + final String tr_remove_contact = Tr.tr("Chats and messages will not be deleted."); + final String tr_not_supported = Tr.tr("Not supported by server"); + private Control.Status mCurrentStatus; private EnumSet mServerFeatures; @@ -208,7 +209,7 @@ public void run() { private void updateOnEDT(Object arg) { if (arg instanceof ViewEvent.StatusChange) { ViewEvent.StatusChange statChange = (ViewEvent.StatusChange) arg; - this.statusChanged(statChange.status, mServerFeatures); + this.statusChanged(statChange.status, statChange.features); } else if (arg instanceof ViewEvent.PasswordSet) { this.showPasswordDialog(false); } else if (arg instanceof ViewEvent.MissingAccount) { @@ -245,12 +246,13 @@ private void updateOnEDT(Object arg) { private void statusChanged(Control.Status status, EnumSet features) { mCurrentStatus = status; mServerFeatures = features; + + mChatView.onStatusChange(status, features); switch (status) { case CONNECTING: mStatusBarLabel.setText(Tr.tr("Connecting…")); break; case CONNECTED: - mChatView.setColor(Color.WHITE); mStatusBarLabel.setText(Tr.tr("Connected")); NotificationManager.hideAllNotifications(); break; @@ -258,7 +260,6 @@ private void statusChanged(Control.Status status, EnumSet mStatusBarLabel.setText(Tr.tr("Disconnecting…")); break; case DISCONNECTED: - mChatView.setColor(Color.LIGHT_GRAY); mStatusBarLabel.setText(Tr.tr("Not connected")); //if (mTrayIcon != null) // trayIcon.setImage(updatedImage); @@ -274,7 +275,6 @@ private void statusChanged(Control.Status status, EnumSet mStatusBarLabel.setText(Tr.tr("Connecting failed")); break; case ERROR: - mChatView.setColor(Color.lightGray); mStatusBarLabel.setText(Tr.tr("Connection error")); break; } From e62fd994010587b9ce9a0d1c1288b37c03fd2e05 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 18:45:19 +0100 Subject: [PATCH 152/257] view: use chat directly instead of ID for cache map in chat view --- src/main/java/org/kontalk/view/ChatView.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 55b174d7..47281bdb 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -103,7 +103,7 @@ final class ChatView extends WebPanel implements Observer { private final WebFileChooser mFileChooser; private final WebButton mFileButton; - private final Map mChatCache = new HashMap<>(); + private final Map mMessageListCache = new HashMap<>(); private ComponentUtils.ModalPopup mPopup = null; private Background mDefaultBG; @@ -292,13 +292,13 @@ void showChat(Chat chat) { chat.addObserver(this); - if (!mChatCache.containsKey(chat.getID())) { + if (!mMessageListCache.containsKey(chat)) { MessageList newMessageList = new MessageList(mView, this, chat); chat.addObserver(newMessageList); - mChatCache.put(chat.getID(), newMessageList); + mMessageListCache.put(chat, newMessageList); } // set to current chat - mScrollPane.getViewport().setView(mChatCache.get(chat.getID())); + mScrollPane.getViewport().setView(mMessageListCache.get(chat)); this.onChatChange(); chat.setRead(); @@ -412,12 +412,11 @@ private void updateOnEDT(Object arg) { if (arg instanceof Chat) { Chat chat = (Chat) arg; if (chat.isDeleted()) { - MessageList viewList = mChatCache.get(chat.getID()); + MessageList viewList = mMessageListCache.remove(chat); if (viewList != null) { viewList.clearItems(); chat.deleteObserver(viewList); } - mChatCache.remove(chat.getID()); if(this.getCurrentChat().orElse(null) == chat) { mScrollPane.setViewportView(null); mView.showNothing(); From 7a8bbd3da0dffee41eec7a14acf9a5c9a1feb9bf Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 19:29:34 +0100 Subject: [PATCH 153/257] model: fix member instantiation when creating/loading single chat --- .../java/org/kontalk/model/chat/Chat.java | 10 ++------- .../java/org/kontalk/model/chat/ChatList.java | 2 +- .../java/org/kontalk/model/chat/Member.java | 21 +++++++++---------- .../org/kontalk/model/chat/SingleChat.java | 18 ++++++++-------- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 4bceb816..28b0d51b 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -22,7 +22,6 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -79,10 +78,6 @@ public abstract class Chat extends Observable implements Observer { private ViewSettings mViewSettings; - protected Chat(Contact contact, String xmppID, String subject) { - this(Arrays.asList(new Member(contact)), xmppID, subject, null); - } - protected Chat(List members, String xmppID, String subject, GroupMetaData gData) { mMessages = new ChatMessages(this, true); mRead = true; @@ -102,8 +97,7 @@ protected Chat(List members, String xmppID, String subject, GroupMetaDat return; } - for (Member member : members) - member.insert(mID); + members.stream().forEach(member -> member.insert(mID)); } // used when loading from database @@ -293,7 +287,7 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { LOGGER.warning("not one contact for single chat, id="+id); return null; } - return new SingleChat(id, members.get(0).getContact(), xmppID, read, jsonViewSettings); + return new SingleChat(id, members.get(0), xmppID, read, jsonViewSettings); } } diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 6864f72f..1441a433 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -116,7 +116,7 @@ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { } private SingleChat createNew(Contact contact, String xmppThreadID) { - SingleChat newChat = new SingleChat(contact, xmppThreadID); + SingleChat newChat = new SingleChat(new Member(contact), xmppThreadID); LOGGER.config("new single chat: "+newChat); this.putSilent(newChat); this.changed(newChat); diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index 258acdd4..4c32a23d 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -49,7 +49,6 @@ public final class Member { */ public enum Role {DEFAULT, OWNER, ADMIN}; - // many to many relationship requires additional table for members public static final String TABLE = "receiver"; public static final String COL_CONTACT_ID = "user_id"; public static final String COL_ROLE = "role"; @@ -67,7 +66,7 @@ public enum Role {DEFAULT, OWNER, ADMIN}; private final Contact mContact; private final Role mRole; - private int id; + private int mID; private ChatState mState = ChatState.gone; // note: the Android client does not set active states when only viewing @@ -85,9 +84,9 @@ public Member(Contact contact, Role role) { } private Member(int id, Contact contact, Role role) { - this.id = id; - this.mContact = contact; - this.mRole = role; + mID = id; + mContact = contact; + mRole = role; } public Contact getContact() { @@ -119,7 +118,7 @@ public int hashCode() { @Override public String toString() { - return "Mem:c={"+mContact+"}r="+mRole; + return "Mem:cont={"+mContact+"},role="+mRole; } public ChatState getState() { @@ -127,7 +126,7 @@ public ChatState getState() { } boolean insert(int chatID) { - if (id > 0) { + if (mID > 0) { LOGGER.warning("already in database"); return true; } @@ -136,8 +135,8 @@ boolean insert(int chatID) { recValues.add(chatID); recValues.add(getContact().getID()); recValues.add(mRole); - id = Database.getInstance().execInsert(TABLE, recValues); - if (id <= 0) { + mID = Database.getInstance().execInsert(TABLE, recValues); + if (mID <= 0) { LOGGER.warning("could not insert member"); return false; } @@ -149,12 +148,12 @@ void save() { } boolean delete() { - if (id <= 0) { + if (mID <= 0) { LOGGER.warning("not in database"); return true; } - return Database.getInstance().execDelete(TABLE, id); + return Database.getInstance().execDelete(TABLE, mID); } protected void setState(ChatState state) { diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index 5bdc4f2c..b18b2d6f 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -35,26 +35,26 @@ public final class SingleChat extends Chat { private final Member mMember; private final String mXMPPID; - SingleChat(Contact contact, String xmppID) { - super(contact, xmppID, ""); + SingleChat(Member member, String xmppID) { + super(Arrays.asList(member), xmppID, "", null); - mMember = new Member(contact); - contact.addObserver(this); - // note: Kontalk Android client is ignoring the chat id + mMember = member; + mMember.getContact().addObserver(this); + // NOTE: Kontalk Android client is ignoring the chat XMPP-ID mXMPPID = xmppID; } // used when loading from database SingleChat(int id, - Contact contact, + Member member, String xmppID, boolean read, String jsonViewSettings ) { super(id, read, jsonViewSettings); - mMember = new Member(contact); - contact.addObserver(this); + mMember = member; + mMember.getContact().addObserver(this); mXMPPID = xmppID; } @@ -148,6 +148,6 @@ public int hashCode() { @Override public String toString() { - return "SC:id="+mID+",xmppid="+mXMPPID; + return "SC:id="+mID+",xmppid="+mXMPPID+",mem="+mMember; } } From d98e4f001e8704f128401b3825d282e3d2b452c2 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 19:32:08 +0100 Subject: [PATCH 154/257] model: not using chat ID in chat list anymore --- .../java/org/kontalk/model/chat/ChatList.java | 33 ++++++++----------- src/main/java/org/kontalk/system/Control.java | 2 +- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 1441a433..b1c6ac4c 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -21,11 +21,9 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Optional; @@ -44,15 +42,14 @@ public final class ChatList extends Observable implements Observer, Iterable mMap = - Collections.synchronizedMap(new HashMap()); + private final Set mChats = Collections.synchronizedSet(new HashSet()); private boolean mUnread = false; private ChatList() {} public void load() { - assert mMap.isEmpty(); + assert mChats.isEmpty(); Database db = Database.getInstance(); try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) { @@ -71,12 +68,12 @@ public void load() { } public Set getAll() { - return new HashSet<>(mMap.values()); + return new HashSet<>(mChats); } /** Get single chat with contact and XMPPID. */ public Optional get(Contact contact, String xmmpThreadID) { - for (Chat chat : mMap.values()) { + for (Chat chat : mChats) { if (!(chat instanceof SingleChat)) continue; SingleChat singleChat = (SingleChat) chat; @@ -89,14 +86,13 @@ public Optional get(Contact contact, String xmmpThreadID) { } public Optional get(GroupMetaData gData) { - for (Chat chat : mMap.values()) { + for (Chat chat : mChats) { if (!(chat instanceof GroupChat)) continue; GroupChat groupChat = (GroupChat) chat; if (groupChat.getGroupData().equals(gData)) return Optional.of(groupChat); - } return Optional.empty(); @@ -136,12 +132,11 @@ public GroupChat createNew(List members, GroupMetaData gData, String sub } private void putSilent(Chat chat) { - if (mMap.containsValue(chat)) { + boolean succ = mChats.add(chat); + if (!succ) { LOGGER.warning("chat already in chat list: "+chat); return; } - - mMap.put(chat.getID(), chat); chat.addObserver(this); } @@ -150,13 +145,13 @@ public boolean contains(Contact contact) { } public boolean isEmpty() { - return mMap.isEmpty(); + return mChats.isEmpty(); } - public void delete(int id) { - Chat chat = mMap.remove(id); - if (chat == null) { - LOGGER.warning("can't delete chat, not found. id: "+id); + public void delete(Chat chat) { + boolean succ = mChats.remove(chat); + if (!succ) { + LOGGER.warning("can't delete chat, not found: "+chat); return; } chat.delete(); @@ -190,7 +185,7 @@ public void update(Observable o, Object arg) { return; } - for (Chat chat : mMap.values()) { + for (Chat chat : mChats) { if (!chat.isRead()) { return; } @@ -202,7 +197,7 @@ public void update(Observable o, Object arg) { @Override public Iterator iterator() { - return mMap.values().iterator(); + return mChats.iterator(); } public static ChatList getInstance() { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index c4193b33..a6aad29c 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -715,7 +715,7 @@ public void deleteChat(Chat chat) { return; } - ChatList.getInstance().delete(chat.getID()); + ChatList.getInstance().delete(chat); } public void setChatSubject(GroupChat chat, String subject) { From 6a3926ea66d00caa73e2d02b1c48db9aaa33bab3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 19 Feb 2016 19:38:51 +0100 Subject: [PATCH 155/257] i18n: (auto)update translation strings --- src/main/resources/i18n/strings.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index 58e9f6dd..c2263bcf 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -185,7 +185,6 @@ s_PBTX = Not signed s_SIR1 = Verified s_WN8A = Supported files s_CQEO = File -s_1PWT = Send File - max. size: s_7MCH = Received new key for contact s_2B02 = The key for this contact could not yet be received s_KGM6 = stalled @@ -257,3 +256,5 @@ s_RAMV = Medium (0.5MP) s_7CPG = Large (0.8MP) s_5RTB = Reduce size of images before sending s_13FX = Resize image attachments: +s_8OBK = Send File +s_7YWF = max. size: From 27486b59c6f8445c303ad80c22ce2628cb408bd4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 20 Feb 2016 18:34:15 +0100 Subject: [PATCH 156/257] model: fixed findFirst() Streams are nice --- .../org/kontalk/model/chat/ChatMessages.java | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index ff554fc5..473c12de 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -18,7 +18,6 @@ package org.kontalk.model.chat; -import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; @@ -117,19 +116,9 @@ public SortedSet getPending() { public Optional getLast(String xmppID) { this.ensureLoaded(); - // TODO performance - OutMessage message = null; - for (KonMessage m: mSet.descendingSet()) { - if (m.getXMPPID().equals(xmppID) && m instanceof OutMessage) { - message = (OutMessage) m; - } - } - - if (message == null) { - return Optional.empty(); - } - - return Optional.of(message); + return mSet.descendingSet().stream() + .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) + .map(m -> (OutMessage) m).findFirst(); } /** Get the last created message. */ From eecf8164f563fb391c9fd9d671d5f87794b71e18 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 20 Feb 2016 19:07:25 +0100 Subject: [PATCH 157/257] model: identify if group chat is administratable by own role --- .../java/org/kontalk/model/chat/GroupChat.java | 8 +++++++- .../java/org/kontalk/model/chat/GroupMetaData.java | 14 -------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 6ad02576..ca534c3c 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -264,7 +264,13 @@ public boolean isValid() { @Override public boolean isAdministratable() { - return mGroupData.isAdministratable(); + Member me = mMemberSet.stream() + .filter(m -> m.getContact().isMe()) + .findFirst().orElse(null); + if (me == null) + return false; + Member.Role myRole = me.getRole(); + return myRole == Member.Role.OWNER || myRole == Member.Role.ADMIN; } private boolean containsMe() { diff --git a/src/main/java/org/kontalk/model/chat/GroupMetaData.java b/src/main/java/org/kontalk/model/chat/GroupMetaData.java index 599e8e58..561971c1 100644 --- a/src/main/java/org/kontalk/model/chat/GroupMetaData.java +++ b/src/main/java/org/kontalk/model/chat/GroupMetaData.java @@ -34,9 +34,6 @@ public abstract class GroupMetaData { private static final Logger LOGGER = Logger.getLogger(GroupMetaData.class.getName()); - // TODO move role/affiliation data to group chat class - abstract boolean isAdministratable(); - abstract String toJSON(); /** Data fields specific to a Kontalk group chat (custom protocol). */ @@ -52,11 +49,6 @@ public KonGroupData(JID ownerJID, String id) { this.id = id; } - @Override - boolean isAdministratable() { - return owner.isMe(); - } - // using legacy lib, raw types extend Object @SuppressWarnings(value = "unchecked") @Override @@ -116,12 +108,6 @@ public MUCData(JID room, String password) { this.password = password; } - @Override - boolean isAdministratable() { - // TODO - return true; - } - // using legacy lib, raw types extend Object @SuppressWarnings(value = "unchecked") @Override From ce140cadb36c9b49a443a3c3a162bedfb8f60e1f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 20 Feb 2016 19:31:21 +0100 Subject: [PATCH 158/257] model: fixed equals() in Member --- src/main/java/org/kontalk/model/chat/Member.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index 4c32a23d..b6f0bd26 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -106,7 +106,7 @@ public boolean equals(Object o) { return false; // TODO dangerous - return mContact.equals(o); + return mContact.equals(((Member) o).mContact); } @Override From f39d4d3b2bde912605fa81252b889f6224a63e30 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 20 Feb 2016 19:34:45 +0100 Subject: [PATCH 159/257] model: equals() for Contact --- src/main/java/org/kontalk/model/Contact.java | 18 ++++++++++++++++++ .../java/org/kontalk/model/chat/Member.java | 1 - 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index b0856354..6ab10544 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -339,6 +339,24 @@ private void changed(Object arg) { this.notifyObservers(arg); } + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Contact)) + return false; + + return mID == ((Contact) o).mID; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 29 * hash + this.mID; + return hash; + } + @Override public String toString() { return "C:id="+mID+",jid="+mJID+",name="+mName+",fp="+mFingerprint diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index b6f0bd26..8b8aa22c 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -105,7 +105,6 @@ public boolean equals(Object o) { if (!(o instanceof Member)) return false; - // TODO dangerous return mContact.equals(((Member) o).mContact); } From 1187d622ed9ea69be8840345dd336889bf576cb1 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 21 Feb 2016 20:05:55 +0100 Subject: [PATCH 160/257] model: fixed NPE when deleting contact --- src/main/java/org/kontalk/model/Contact.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 6ab10544..0fe30b59 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -309,7 +309,8 @@ void setDeleted() { mEncrypted = false; mKey = ""; mFingerprint = ""; - mAvatar.delete(); + if (mAvatar != null) + mAvatar.delete(); mAvatar = null; this.save(); From fc13c39c087a59f4a3673f9a3a1ed78507833181 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 21 Feb 2016 20:18:58 +0100 Subject: [PATCH 161/257] misc: better JID validation --- src/main/java/org/kontalk/misc/JID.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 21e53868..17d2ec10 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -20,6 +20,8 @@ import java.util.Objects; import org.apache.commons.lang.StringUtils; +import org.jxmpp.jid.util.JidUtil; +import org.jxmpp.stringprep.simple.SimpleXmppStringprep; import org.jxmpp.util.XmppStringUtils; import org.kontalk.model.Account; @@ -34,6 +36,11 @@ public final class JID { private final String mDomain; private final String mResource; + static { + // good to know. For working JID validation + SimpleXmppStringprep.setup(); + } + private JID(String local, String domain, String resource) { mLocal = local; mDomain = domain; @@ -53,9 +60,12 @@ public String string() { } public boolean isValid() { - // TODO stronger check here. - //org.jxmpp.jid.util.JidUtil.validateBareJid(mBareJID); - return !mLocal.isEmpty() && !mDomain.isEmpty(); + if (mLocal.isEmpty() || mDomain.isEmpty()) + return false; + + // NOTE: domain check could be stronger - complaint with RFC 6122, but + // server does not accept special characters + return JidUtil.isValidBareJid(this.string()); } public boolean isHash() { From 0c017ef3560cbc51f345e055927a4cae54bfac27 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 22 Feb 2016 17:57:09 +0100 Subject: [PATCH 162/257] view: cleaner code for sync in list views --- .../java/org/kontalk/view/ChatListView.java | 14 ++++---- .../org/kontalk/view/ContactListView.java | 14 ++++---- src/main/java/org/kontalk/view/ListView.java | 23 +++++++----- .../java/org/kontalk/view/MessageList.java | 35 ++++++------------- 4 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 4b99e668..e9c497b8 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -29,8 +29,6 @@ import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.util.HashSet; -import java.util.Set; import java.util.Timer; import javax.swing.Box; import javax.swing.ListSelectionModel; @@ -93,12 +91,12 @@ public void valueChanged(ListSelectionEvent e) { @Override protected void updateOnEDT(Object arg) { - Set newItems = new HashSet<>(); - Set chats = mChatList.getAll(); - for (Chat chat: chats) - if (!this.containsValue(chat)) - newItems.add(new ChatItem(chat)); - this.sync(chats, newItems); + this.sync(mChatList.getAll()); + } + + @Override + protected ChatItem newItem(Chat value) { + return new ChatItem(value); } void selectLastChat() { diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index dc9c04d8..c826faf9 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -31,9 +31,7 @@ import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.HashSet; import java.util.Observer; -import java.util.Set; import javax.swing.Box; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; @@ -87,12 +85,12 @@ public void mouseClicked(MouseEvent e) { @Override protected void updateOnEDT(Object arg) { - Set newItems = new HashSet<>(); - Set contacts = Utils.allContacts(); - for (Contact contact: contacts) - if (!this.containsValue(contact)) - newItems.add(new ContactItem(contact)); - this.sync(contacts, newItems); + this.sync(Utils.allContacts()); + } + + @Override + protected ContactItem newItem(Contact value) { + return new ContactItem(value); } @Override diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index 3f0ccf2a..dae5240e 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -195,12 +195,8 @@ private void showPopupMenu(MouseEvent e, I item) { protected abstract WebPopupMenu rightClickMenu(I item); - protected boolean containsValue(V value) { - return mItems.containsKey(value); - } - @SuppressWarnings("unchecked") - protected void sync(Set values, Set newItems) { + protected boolean sync(Set values) { // TODO performance // remove old for (int i=0; i < mModel.getRowCount(); i++) { @@ -210,16 +206,25 @@ protected void sync(Set values, Set newItems) { item.mValue.deleteObserver(item); mModel.removeRow(i); i--; + mItems.remove(item.mValue); } } // add new - for (I item : newItems) { - item.mValue.addObserver(item); - mItems.put(item.mValue, item); - mModel.addRow(new Object[]{item}); + boolean added = false; + for (V v: values) { + if (!mItems.containsKey(v)) { + I item = this.newItem(v); + item.mValue.addObserver(item); + mItems.put(item.mValue, item); + mModel.addRow(new Object[]{item}); + added = true; + } } + return added; } + protected abstract I newItem(V value); + @SuppressWarnings("unchecked") protected I getDisplayedItemAt(int i) { return (I) mModel.getValueAt(mRowSorter.convertRowIndexToModel(i), 0); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index f6c9b195..fcd053a5 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -36,7 +36,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; -import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -144,38 +143,26 @@ protected void updateOnEDT(Object arg) { return; } - if (arg instanceof KonMessage) { - this.insertMessage((KonMessage) arg); - } else { - // check for new messages to add - if (this.getModel().getRowCount() < mChat.getMessages().size()) - this.insertMessages(); + // check for new messages to add + if (this.getModel().getRowCount() < mChat.getMessages().size()) { + this.insertMessages(); } - if (mChatView.getCurrentChat().orElse(null) == mChat) { + if (!mChat.isRead() && mChatView.getCurrentChat().orElse(null) == mChat) { mChat.setRead(); } } private void insertMessages() { - Set newItems = new HashSet<>(); - Set messages = mChat.getMessages().getAll(); - for (KonMessage message: messages) { - if (!this.containsValue(message)) { - newItems.add(new MessageItem(message)); - // trigger scrolling - mChatView.setScrolling(); - } - } - this.sync(messages, newItems); + boolean newAdded = this.sync(mChat.getMessages().getAll()); + if (newAdded) + // trigger scrolling + mChatView.setScrolling(); } - private void insertMessage(KonMessage message) { - Set newItems = new HashSet<>(); - newItems.add(new MessageItem(message)); - this.sync(mChat.getMessages().getAll(), newItems); - // trigger scrolling - mChatView.setScrolling(); + @Override + protected MessageItem newItem(KonMessage value) { + return new MessageItem(value); } private void setBackground(Chat.ViewSettings s) { From 15f9ef7b96cb4bfca4ce000b4ce5488b7113d4b5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 22 Feb 2016 18:09:20 +0100 Subject: [PATCH 163/257] model: return unmodifiable sets to view --- src/main/java/org/kontalk/model/ContactList.java | 10 +++++----- src/main/java/org/kontalk/model/chat/ChatList.java | 2 +- src/main/java/org/kontalk/model/chat/ChatMessages.java | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 07623375..10f9617d 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -22,7 +22,6 @@ import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Observable; @@ -126,10 +125,11 @@ public Optional getMe() { return this.create(myJID, ""); } - public Set getAll(final boolean withMe) { - return mJIDMap.values().stream() - .filter(c -> (withMe || !c.isMe())) - .collect(Collectors.toCollection(HashSet::new)); + public Set getAll(boolean withMe) { + return Collections.unmodifiableSet( + mJIDMap.values().stream() + .filter(c -> (withMe || !c.isMe())) + .collect(Collectors.toSet())); } public void delete(Contact contact) { diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index b1c6ac4c..046ac493 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -68,7 +68,7 @@ public void load() { } public Set getAll() { - return new HashSet<>(mChats); + return Collections.unmodifiableSet(mChats); } /** Get single chat with contact and XMPPID. */ diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 473c12de..b95ba094 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -35,6 +35,8 @@ /** * Messages of chat. * + * Sorted by creation time. + * * @author Alexander Bikadorov {@literal } */ public final class ChatMessages { @@ -98,7 +100,7 @@ private boolean addSilent(KonMessage message) { public NavigableSet getAll() { this.ensureLoaded(); - return mSet; + return Collections.unmodifiableNavigableSet(mSet); } /** Get all outgoing messages with status "PENDING" for this chat. */ From 237b4eba39f078313d68a66ed3d2375d39c977f7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 23 Feb 2016 16:54:49 +0100 Subject: [PATCH 164/257] model: implemented equals() for messages --- .../org/kontalk/model/message/InMessage.java | 22 +++++++++++++++++- .../org/kontalk/model/message/KonMessage.java | 23 +++++++++++++++++++ .../org/kontalk/model/message/OutMessage.java | 3 +-- .../kontalk/model/message/Transmission.java | 23 +++++++++++++++++++ src/main/java/org/kontalk/system/Control.java | 1 - 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/model/message/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java index b20c5134..8dd316ca 100644 --- a/src/main/java/org/kontalk/model/message/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -21,6 +21,7 @@ import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; import java.util.Date; +import java.util.Objects; import java.util.Optional; import java.util.logging.Logger; import org.kontalk.crypto.Coder; @@ -29,7 +30,7 @@ import org.kontalk.model.message.MessageContent.Preview; /** - * Model for an XMPP message that was sent to us. + * Model for an XMPP message sent to the user. * @author Alexander Bikadorov {@literal } */ public final class InMessage extends KonMessage implements DecryptMessage { @@ -138,4 +139,23 @@ public void setPreviewFilename(String filename) { public Transmission[] getTransmissions() { return new Transmission[]{mTransmission}; } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof InMessage)) + return false; + + return super.equals(o) && + mTransmission.equals(((InMessage) o).mTransmission); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 67 * hash + Objects.hashCode(this.mTransmission); + return hash; + } } diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index 445e8d6a..e5e57a92 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -28,6 +28,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Observable; import java.util.Optional; import java.util.logging.Level; @@ -264,6 +265,28 @@ protected void changed(Object arg) { this.notifyObservers(arg); } + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof KonMessage)) + return false; + + KonMessage oMessage = (KonMessage) o; + + return mChat.equals(oMessage.mChat) + && !mXMPPID.isEmpty() && mXMPPID.equals(oMessage.mXMPPID); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 17 * hash + Objects.hashCode(this.mChat); + hash = 17 * hash + Objects.hashCode(this.mXMPPID); + return hash; + } + @Override public String toString() { return "M:id="+mID+",status="+mStatus+",chat="+mChat+",xmppid="+mXMPPID diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index f6488d39..8c798402 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -30,7 +30,7 @@ import org.kontalk.model.Contact; /** - * Model for an XMPP message that we are sending. + * Model for an XMPP message from the user to a contact. * @author Alexander Bikadorov {@literal } */ public final class OutMessage extends KonMessage { @@ -123,5 +123,4 @@ public void setUpload(URI url, String mime, long length) { public Transmission[] getTransmissions() { return mTransmissions; } - } diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index 7e052b66..b989931a 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -27,6 +27,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -163,4 +164,26 @@ private static Transmission load(ResultSet resultSet) throws SQLException { return new Transmission(id, contact, jid, receivedDate); } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Transmission)) + return false; + + Transmission oTransmission = (Transmission) o; + + return mContact.equals(oTransmission.mContact) + && mJID.equals(oTransmission.mJID); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 71 * hash + Objects.hashCode(this.mContact); + hash = 71 * hash + Objects.hashCode(this.mJID); + return hash; + } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index a6aad29c..26842422 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -198,7 +198,6 @@ public void onNewInMessage(MessageIDs ids, if (newMessage.getID() <= 0) return; - // TODO implement equals() if (chat.getMessages().contains(newMessage)) { LOGGER.info("message already in chat, dropping this one"); return; From 789e35791ff95461ee9d64d03c3077287a49c1f2 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 24 Feb 2016 20:36:19 +0100 Subject: [PATCH 165/257] model: replaced arrays with set --- .../java/org/kontalk/client/KonMessageSender.java | 3 +-- src/main/java/org/kontalk/crypto/Encryptor.java | 2 +- .../java/org/kontalk/model/chat/ChatMessages.java | 2 +- .../java/org/kontalk/model/message/InMessage.java | 11 +++++++---- .../org/kontalk/model/message/KonMessage.java | 10 +++++----- .../org/kontalk/model/message/OutMessage.java | 15 +++++++++------ .../org/kontalk/model/message/Transmission.java | 12 +++++++----- src/main/java/org/kontalk/view/MessageList.java | 6 +++--- 8 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index b45b10f6..227109b1 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -111,8 +111,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { } // transmission specific - Transmission[] transmissions = message.getTransmissions(); - ArrayList sendMessages = new ArrayList<>(transmissions.length); + ArrayList sendMessages = new ArrayList<>(); for (Transmission transmission: message.getTransmissions()) { Message sendMessage = protoMessage.clone(); JID to = transmission.getJID(); diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index 9be4ff61..328f2daa 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -151,7 +151,7 @@ private boolean loadKeys() { message.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); return false; } - List contacts = new ArrayList<>(message.getTransmissions().length); + List contacts = new ArrayList<>(); for (Transmission t : message.getTransmissions()) contacts.add(t.getContact()); receiverKeys = receiverKeysOrNull(contacts.toArray(new Contact[0])); diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index b95ba094..c54aedbd 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -69,7 +69,7 @@ private void loadMessages() { KonMessage.COL_CHAT_ID + " == " + mChat.getID())) { while (messageRS.next()) { KonMessage message = KonMessage.load(messageRS, mChat); - if (message.getTransmissions().length == 0) + if (message.getTransmissions().isEmpty()) // ignore broken message continue; this.addSilent(message); diff --git a/src/main/java/org/kontalk/model/message/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java index 8dd316ca..ee546fe3 100644 --- a/src/main/java/org/kontalk/model/message/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -18,11 +18,14 @@ package org.kontalk.model.message; +import java.util.Arrays; import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; import java.util.Date; +import java.util.HashSet; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; @@ -54,10 +57,10 @@ public InMessage(ProtoMessage proto, Chat chat, JID from, String xmppID, protected InMessage(KonMessage.Builder builder) { super(builder); - if (builder.mTransmissions.length != 1) + if (builder.mTransmissions.size() != 1) throw new IllegalArgumentException("builder does not contain one transmission"); - mTransmission = builder.mTransmissions[0]; + mTransmission = builder.mTransmissions.stream().findAny().get(); } @Override @@ -136,8 +139,8 @@ public void setPreviewFilename(String filename) { } @Override - public Transmission[] getTransmissions() { - return new Transmission[]{mTransmission}; + public Set getTransmissions() { + return new HashSet<>(Arrays.asList(mTransmission)); } @Override diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index e5e57a92..d3d73e3a 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -21,7 +21,6 @@ import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; @@ -31,6 +30,7 @@ import java.util.Objects; import java.util.Observable; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.json.simple.JSONObject; @@ -183,7 +183,7 @@ public boolean isInMessage() { return mStatus == Status.IN; } - public abstract Transmission[] getTransmissions(); + public abstract Set getTransmissions(); public String getXMPPID() { return mXMPPID; @@ -290,7 +290,7 @@ public int hashCode() { @Override public String toString() { return "M:id="+mID+",status="+mStatus+",chat="+mChat+",xmppid="+mXMPPID - +",transmissions="+Arrays.toString(this.getTransmissions()) + +",transmissions="+this.getTransmissions() +",date="+mDate+",sdate="+mServerDate +",cont="+mContent +",codstat="+mCoderStatus+",serverr="+mServerError; @@ -395,7 +395,7 @@ protected static class Builder { private final Date mDate; private final MessageContent mContent; - protected Transmission[] mTransmissions = null; + protected Set mTransmissions = null; private String mXMPPID = null; private Date mServerDate = null; @@ -414,7 +414,7 @@ private Builder(int id, mContent = content; } - private void transmissions(Transmission[] transmission) { mTransmissions = transmission; } + private void transmissions(Set transmission) { mTransmissions = transmission; } private void xmppID(String xmppID) { mXMPPID = xmppID; } private void serverDate(Date date) { mServerDate = date; } diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 8c798402..d065c02b 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -21,6 +21,7 @@ import org.kontalk.model.chat.Chat; import org.kontalk.misc.JID; import java.net.URI; +import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Optional; @@ -36,7 +37,7 @@ public final class OutMessage extends KonMessage { private static final Logger LOGGER = Logger.getLogger(OutMessage.class.getName()); - private final Transmission[] mTransmissions; + private final Set mTransmissions; public OutMessage(Chat chat, Contact[] contacts, MessageContent content, boolean encrypted) { super(chat, @@ -48,18 +49,20 @@ public OutMessage(Chat chat, Contact[] contacts, MessageContent content, boolean CoderStatus.createToEncrypt() : CoderStatus.createInsecure()); - Set t = new HashSet<>(); + Set ts = new HashSet<>(); for (Contact contact: contacts) { - t.add(new Transmission(contact, contact.getJID(), mID)); + boolean succ = ts.add(new Transmission(contact, contact.getJID(), mID)); + if (!succ) + LOGGER.warning("duplicate contact: "+contact); } - mTransmissions = t.toArray(new Transmission[0]); + mTransmissions = Collections.unmodifiableSet(ts); } // used when loading from database protected OutMessage(KonMessage.Builder builder) { super(builder); - mTransmissions = builder.mTransmissions; + mTransmissions = Collections.unmodifiableSet(builder.mTransmissions); } public void setReceived(JID jid) { @@ -120,7 +123,7 @@ public void setUpload(URI url, String mime, long length) { } @Override - public Transmission[] getTransmissions() { + public Set getTransmissions() { return mTransmissions; } } diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index b989931a..ad59c62e 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -21,14 +21,16 @@ import org.kontalk.misc.JID; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.kontalk.model.Contact; @@ -132,9 +134,9 @@ public String toString() { return "T:id="+mID+",contact="+mContact+",jid="+mJID+",recdate="+mReceivedDate; } - static Transmission[] load(int messageID) { + static Set load(int messageID) { Database db = Database.getInstance(); - ArrayList ts = new ArrayList<>(); + HashSet ts = new HashSet<>(); try (ResultSet transmissionRS = db.execSelectWhereInsecure(TABLE, COL_MESSAGE_ID + " == " + messageID)) { while (transmissionRS.next()) { @@ -142,11 +144,11 @@ static Transmission[] load(int messageID) { } } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't load transmission(s) from db", ex); - return new Transmission[0]; + return Collections.emptySet(); } if (ts.isEmpty()) LOGGER.warning("no transmission(s) found, messageID: "+messageID); - return ts.toArray(new Transmission[0]); + return ts; } private static Transmission load(ResultSet resultSet) throws SQLException { diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index fcd053a5..bd61d7fe 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -411,9 +411,9 @@ private void updateStatus() { boolean isOut = !mValue.isInMessage(); Date deliveredDate = null; - Transmission[] transmissions = mValue.getTransmissions(); - if (transmissions.length == 1) - deliveredDate = transmissions[0].getReceivedDate().orElse(null); + Set transmissions = mValue.getTransmissions(); + if (transmissions.size() == 1) + deliveredDate = transmissions.stream().findFirst().get().getReceivedDate().orElse(null); // status icon if (isOut) { From 795533abd234bdb5f39d6746ef63e6f1e421f48f Mon Sep 17 00:00:00 2001 From: Naofumi Date: Sun, 28 Feb 2016 06:19:12 +0000 Subject: [PATCH 166/257] Translated using Weblate (Japanese) Currently translated at 100.0% (249 of 249 strings) --- src/main/resources/i18n/strings_ja.properties | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/resources/i18n/strings_ja.properties b/src/main/resources/i18n/strings_ja.properties index cb9fefd4..8cf55124 100644 --- a/src/main/resources/i18n/strings_ja.properties +++ b/src/main/resources/i18n/strings_ja.properties @@ -291,3 +291,26 @@ s_KRQ6=パスワードを入力… s_2R2P=ロード中… s_307W=ダウンロード中… s_4WC0=サーバー証明書は検証されていません。 +s_68LN=リクエスト +s_7MDX=連絡先からのリクエストステータス承認 +s_OTEU=自動的に承認を許可 +s_MFZY=自動的に他のユーザからのオンラインステータス承認のリクエストを許可します +s_FAS6=他のユーザーにチャットのアクティビティを送信する (入力中,…) +s_29CW=連絡先を編集 +s_AGYA=連絡先の設定を編集 +s_MNIJ=待機中... +s_0WUC=未検証 +s_9S4L=承認リクエスト +s_FDTC=承認すると、この連絡先をあなたのオンラインステータスに表示することができます。 +s_TAG1=ユーザープロファイル +s_84VP=あなたのプロファイルを編集 +s_4QXX=プロファイル画像: +s_1R7I=プロファイル +s_O2UH=あなたのユーザープロファイルを設定 +s_6SC9=鍵ユーザーID: +s_6K2I=プロファイル画像をダウンロード +s_08GL=連絡先のプロファイル画像をダウンロード +s_T2TO=自分自身を連絡先に表示 +s_F1X0=自分自身を連絡先リストに表示 +s_3PFR=削除 +s_D7FH=サーバーでサポートされていません From abbc2e3b15f15028faffc21cbdb65e28402f1aa3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 24 Feb 2016 20:54:36 +0100 Subject: [PATCH 167/257] model: workaround in chat messags for inconsistency of comparator with equals() Needs some other solution... --- .../org/kontalk/model/chat/ChatMessages.java | 21 +++++++++++-------- .../org/kontalk/model/message/KonMessage.java | 12 ++--------- .../org/kontalk/model/message/OutMessage.java | 18 ++++++++++++++++ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index c54aedbd..fe635c42 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.NavigableSet; import java.util.Optional; +import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; @@ -33,9 +34,7 @@ import org.kontalk.system.Database; /** - * Messages of chat. - * - * Sorted by creation time. + * All messages of a chat. * * @author Alexander Bikadorov {@literal } */ @@ -44,7 +43,10 @@ public final class ChatMessages { private final Chat mChat; private final NavigableSet mSet = - Collections.synchronizedNavigableSet(new TreeSet()); + Collections.synchronizedNavigableSet(new TreeSet( + (KonMessage o1, KonMessage o2) -> { + return o1.getDate().compareTo(o2.getDate()); } + )); private boolean mLoaded; @@ -57,9 +59,9 @@ public final class ChatMessages { private void ensureLoaded() { if (mLoaded) return; + mLoaded = true; this.loadMessages(); - mLoaded = true; } private void loadMessages() { @@ -89,7 +91,7 @@ boolean add(KonMessage message) { } private boolean addSilent(KonMessage message) { - if (mSet.contains(message)) { + if (this.contains(message)) { LOGGER.warning("message already in chat: " + message); return false; } @@ -97,10 +99,10 @@ private boolean addSilent(KonMessage message) { return added; } - public NavigableSet getAll() { + public Set getAll() { this.ensureLoaded(); - return Collections.unmodifiableNavigableSet(mSet); + return Collections.unmodifiableSet(mSet); } /** Get all outgoing messages with status "PENDING" for this chat. */ @@ -133,7 +135,8 @@ public Optional getLast() { public boolean contains(KonMessage message) { this.ensureLoaded(); - return mSet.contains(message); + // TODO ugly stupid workaround (for inconsistency of compareTo() with equal()) + return mSet.stream().anyMatch(m -> m.equals(message)); } public int size() { diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index d3d73e3a..52ce3b42 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -45,7 +45,7 @@ * * @author Alexander Bikadorov {@literal } */ -public abstract class KonMessage extends Observable implements Comparable { +public abstract class KonMessage extends Observable { private static final Logger LOGGER = Logger.getLogger(KonMessage.class.getName()); /** @@ -276,6 +276,7 @@ public boolean equals(Object o) { KonMessage oMessage = (KonMessage) o; return mChat.equals(oMessage.mChat) + && !mXMPPID.isEmpty() && mXMPPID.equals(oMessage.mXMPPID); } @@ -296,15 +297,6 @@ public String toString() { +",codstat="+mCoderStatus+",serverr="+mServerError; } - // TODO remove - @Override - public int compareTo(KonMessage o) { - if (this.equals(o)) - return 0; - - return Integer.compare(mID, o.getID()); - } - public static KonMessage load(ResultSet messageRS, Chat chat) throws SQLException { int id = messageRS.getInt("_id"); diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index d065c02b..92022147 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -126,4 +126,22 @@ public void setUpload(URI url, String mime, long length) { public Set getTransmissions() { return mTransmissions; } + + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + // outmessages are only equal to outmessages + if (!(o instanceof OutMessage)) + return false; + + return super.equals(o); + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + return hash; + } } From b5e563b8b5888ca9c6beaa77bfa9ea8ca07320ec Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 25 Feb 2016 20:21:41 +0100 Subject: [PATCH 168/257] misc: rewrote JID validation --- src/main/java/org/kontalk/misc/JID.java | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 17d2ec10..1bef9024 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -28,23 +28,34 @@ /** * A Jabber ID (the address of an XMPP entity). Immutable. * + * NOTE: manual JID escaping (XEP-0106) is not supported here. Better mark JIDs + * e.g. with spaces in local part as invalid. + * * @author Alexander Bikadorov {@literal } */ public final class JID { - private final String mLocal; - private final String mDomain; - private final String mResource; - static { // good to know. For working JID validation SimpleXmppStringprep.setup(); } + private final String mLocal; + private final String mDomain; + private final String mResource; + private final boolean mValid; + private JID(String local, String domain, String resource) { mLocal = local; mDomain = domain; mResource = resource; + + mValid = !mLocal.isEmpty() && !mDomain.isEmpty() + // NOTE: domain check could be stronger - compliant with RFC 6122, but + // server does not accept most special characters + // NOTE: resource not checked + && JidUtil.isValidBareJid( + XmppStringUtils.completeJidFrom(mLocal, mDomain)); } public String local() { @@ -60,12 +71,7 @@ public String string() { } public boolean isValid() { - if (mLocal.isEmpty() || mDomain.isEmpty()) - return false; - - // NOTE: domain check could be stronger - complaint with RFC 6122, but - // server does not accept special characters - return JidUtil.isValidBareJid(this.string()); + return mValid; } public boolean isHash() { From 57972b0d3094062a65d75cb9794225561c7bbc8c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 26 Feb 2016 18:55:35 +0100 Subject: [PATCH 169/257] crypto: stream usage and arrays replaced --- .../java/org/kontalk/crypto/Encryptor.java | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index 328f2daa..fba13182 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -28,13 +28,14 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.security.SecureRandom; -import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; @@ -51,7 +52,6 @@ import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; import org.kontalk.model.Contact; import org.kontalk.model.message.OutMessage; -import org.kontalk.model.message.Transmission; import org.kontalk.util.CPIMMessage; /** @@ -66,7 +66,7 @@ final class Encryptor { private final OutMessage message; private PersonalKey myKey = null; - private PGPUtils.PGPCoderKey[] receiverKeys = null; + private List receiverKeys = null; Encryptor(OutMessage message) { this.message = message; @@ -93,11 +93,11 @@ private Optional encryptData(String data, String mime) { // secure the message against replay attacks using Message/CPIM String from = myKey.getUserId(); - StringBuilder to = new StringBuilder(); - for (PGPUtils.PGPCoderKey k : receiverKeys) - to.append(k.userID).append("; "); + String to = receiverKeys.stream() + .map(key -> key.userID) + .collect(Collectors.joining("; ")); - CPIMMessage cpim = new CPIMMessage(from, to.toString(), new Date(), mime, data); + CPIMMessage cpim = new CPIMMessage(from, to, new Date(), mime, data); byte[] plainText; try { plainText = cpim.toByteArray(); @@ -151,10 +151,10 @@ private boolean loadKeys() { message.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); return false; } - List contacts = new ArrayList<>(); - for (Transmission t : message.getTransmissions()) - contacts.add(t.getContact()); - receiverKeys = receiverKeysOrNull(contacts.toArray(new Contact[0])); + List contacts = message.getTransmissions().stream() + .map(t -> t.getContact()) + .collect(Collectors.toList()); + receiverKeys = receiverKeysOrNull(contacts); if (receiverKeys == null) { message.setSecurityErrors(EnumSet.of(Coder.Error.KEY_UNAVAILABLE)); return false; @@ -162,15 +162,11 @@ private boolean loadKeys() { return true; } - private static PGPUtils.PGPCoderKey[] receiverKeysOrNull(Contact[] contacts) { - List keys = new ArrayList<>(contacts.length); - for (Contact c : contacts) { - PGPUtils.PGPCoderKey key = Coder.contactkey(c).orElse(null); - if (key == null) - return null; - keys.add(key); - } - return keys.toArray(new PGPUtils.PGPCoderKey[0]); + private static List receiverKeysOrNull(List contacts) { + List keys = contacts.stream() + .map(c -> Coder.contactkey(c).orElse(null)) + .collect(Collectors.toList()); + return keys.stream().anyMatch(Objects::isNull) ? null : keys; } /** @@ -180,7 +176,7 @@ private static PGPUtils.PGPCoderKey[] receiverKeysOrNull(Contact[] contacts) { */ private static void encryptAndSign( InputStream plainInput, OutputStream encryptedOutput, - PersonalKey myKey, PGPUtils.PGPCoderKey[] receiverKeys) + PersonalKey myKey, List receiverKeys) throws IOException, PGPException { // setup data encryptor & generator @@ -190,8 +186,8 @@ private static void encryptAndSign( // add public key recipients PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(encryptor); - for (PGPUtils.PGPCoderKey key : receiverKeys) - encGen.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key.encryptKey)); + receiverKeys.stream().forEach(key -> + encGen.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key.encryptKey))); OutputStream encryptedOut = encGen.open(encryptedOutput, new byte[BUFFER_SIZE]); From b36049ee6e126f211f1a59508db2cf592db89867 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 29 Feb 2016 16:18:02 +0100 Subject: [PATCH 170/257] model: replaced arrays with list and more stream usage --- .../java/org/kontalk/model/chat/Chat.java | 2 +- .../org/kontalk/model/chat/GroupChat.java | 22 +++++++------------ .../org/kontalk/model/chat/SingleChat.java | 9 ++++---- .../org/kontalk/model/message/OutMessage.java | 7 +++--- src/main/java/org/kontalk/system/Control.java | 8 +++---- .../java/org/kontalk/system/GroupControl.java | 10 ++++----- .../java/org/kontalk/view/AvatarLoader.java | 5 ++--- 7 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 28b0d51b..55988941 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -167,7 +167,7 @@ public boolean isGroupChat() { public abstract List getAllContacts(); /** Get valid receiver contacts (without deleted and blocked). */ - public abstract Contact[] getValidContacts(); + public abstract List getValidContacts(); /** XMPP thread ID (empty string if not set). */ public abstract String getXMPPID(); diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index ca534c3c..ae2d4e8c 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -87,11 +87,11 @@ public List getAllContacts() { } @Override - public Contact[] getValidContacts() { + public List getValidContacts() { return mMemberSet.stream() .map(m -> m.getContact()) .filter(c -> (!c.isDeleted() && !c.isMe())) - .toArray(Contact[]::new); + .collect(Collectors.toList()); } public void addContact(Contact contact) { @@ -240,26 +240,20 @@ public String getXMPPID() { @Override public boolean isSendEncrypted() { - boolean encrypted = false; - for (Contact c: this.getValidContacts()) { - encrypted |= c.getEncrypted(); - } - return encrypted; + return this.getValidContacts().stream() + .anyMatch(c -> c.getEncrypted()); } @Override public boolean canSendEncrypted() { - Contact[] contacts = this.getValidContacts(); - boolean encrypted = contacts.length != 0; - for (Contact c: contacts) { - encrypted &= c.hasKey(); - } - return encrypted; + List contacts = this.getValidContacts(); + return !contacts.isEmpty() && + contacts.stream().allMatch(c -> c.hasKey()); } @Override public boolean isValid() { - return this.getValidContacts().length != 0 && this.containsMe(); + return !this.getValidContacts().isEmpty() && this.containsMe(); } @Override diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index b18b2d6f..b1eeaa1c 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -19,6 +19,7 @@ package org.kontalk.model.chat; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.logging.Logger; @@ -73,12 +74,12 @@ public List getAllContacts() { } @Override - public Contact[] getValidContacts() { + public List getValidContacts() { Contact c = mMember.getContact(); - if (c.isDeleted() || c.isBlocked() && !c.isMe()) - return new Contact[0]; + if ((c.isDeleted() || c.isBlocked()) && !c.isMe()) + return Collections.emptyList(); - return new Contact[]{c}; + return Arrays.asList(c); } @Override diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 92022147..9f914e37 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.logging.Logger; @@ -39,7 +40,7 @@ public final class OutMessage extends KonMessage { private final Set mTransmissions; - public OutMessage(Chat chat, Contact[] contacts, MessageContent content, boolean encrypted) { + public OutMessage(Chat chat, List contacts, MessageContent content, boolean encrypted) { super(chat, "Kon_" + StringUtils.randomString(8), content, @@ -50,11 +51,11 @@ public OutMessage(Chat chat, Contact[] contacts, MessageContent content, boolean CoderStatus.createInsecure()); Set ts = new HashSet<>(); - for (Contact contact: contacts) { + contacts.stream().forEach(contact -> { boolean succ = ts.add(new Transmission(contact, contact.getJID(), mID)); if (!succ) LOGGER.warning("duplicate contact: "+contact); - } + }); mTransmissions = Collections.unmodifiableSet(ts); } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 26842422..32fdddbd 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -127,8 +127,8 @@ public void onStatusChange(Status status, EnumSet features mClient.sendUserPresence(strings.length > 0 ? strings[0] : ""); // send all pending messages for (Chat chat: ChatList.getInstance()) - for (OutMessage m : chat.getMessages().getPending()) - this.sendMessage(m); + chat.getMessages().getPending().stream() + .forEach(m -> this.sendMessage(m)); // send public key requests for Kontalk contacts with missing key for (Contact contact : ContactList.getInstance()) @@ -342,8 +342,8 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { return false; } - Contact[] contacts = chat.getValidContacts(); - if (contacts.length == 0) { + List contacts = chat.getValidContacts(); + if (contacts.isEmpty()) { LOGGER.warning("can't send message, no (valid) contact(s)"); return false; } diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 52f9850d..0f2119c3 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -18,11 +18,11 @@ package org.kontalk.system; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.kontalk.misc.JID; import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; @@ -72,12 +72,10 @@ private KonChatControl(KonGroupChat chat) { @Override void onCreate() { - Contact[] contacts = mChat.getValidContacts(); - // send create group command - List jids = new ArrayList<>(contacts.length); - for (Contact c: contacts) - jids.add(c.getJID()); + List jids = mChat.getValidContacts().stream() + .map(contact -> contact.getJID()) + .collect(Collectors.toList()); mControl.createAndSendMessage(mChat, MessageContent.groupCommand( diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index b85288fa..ab27f71c 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -92,9 +92,8 @@ private static class Item { group = true; // nice to have: group picture } else { - Contact[] contacts = chat.getValidContacts(); - if (contacts.length > 0) { - Contact c = contacts[0]; + Contact c = chat.getValidContacts().stream().findFirst().orElse(null); + if (c != null) { a = c.getAvatar().orElse(null); l = c.getName(); cc = hash(c.getID()); From 2f85b9de31ba42ca413a095018fae830f7521f6b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 29 Feb 2016 16:25:30 +0100 Subject: [PATCH 171/257] removed debug println()s --- src/main/java/org/kontalk/model/chat/GroupChat.java | 1 - src/main/java/org/kontalk/util/MediaUtils.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index ae2d4e8c..c12d1bbf 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -113,7 +113,6 @@ public void addContacts(List contacts) { } if (changed) { - System.out.println("addContacts save"); this.save(); this.changed(contacts); } diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 164f37ea..2a9b64ab 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -162,7 +162,6 @@ public static BufferedImage scale(BufferedImage image, int maxPixels) { int ih = image.getHeight(); double scale = Math.sqrt(maxPixels / (iw * ih * 1.0)); - System.out.println("iw= "+iw+" ih="+ih+" maxP="+maxPixels+" scale="+scale); return toBufferedImage(scaleAsync(image, (int) (iw * scale), (int) (ih * scale))); } From 4912b9d0fcb4c9699eb88f9cedd0d40b380c523f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 29 Feb 2016 17:31:43 +0100 Subject: [PATCH 172/257] model: two sets in chat messags cause of inconsistency of comparator with equals() Guess there is no better solution. --- .../org/kontalk/model/chat/ChatMessages.java | 31 +++++++++++-------- .../org/kontalk/model/message/KonMessage.java | 3 +- .../org/kontalk/model/message/OutMessage.java | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index fe635c42..4751c0b9 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -21,6 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; +import java.util.HashSet; import java.util.NavigableSet; import java.util.Optional; import java.util.Set; @@ -42,11 +43,15 @@ public final class ChatMessages { private static final Logger LOGGER = Logger.getLogger(ChatMessages.class.getName()); private final Chat mChat; - private final NavigableSet mSet = + // comparator inconsistent with .equals(); using one set for ordering... + private final NavigableSet mSortedSet = Collections.synchronizedNavigableSet(new TreeSet( (KonMessage o1, KonMessage o2) -> { return o1.getDate().compareTo(o2.getDate()); } )); + // ... and one set for .contains() + private final Set mContainsSet = + Collections.synchronizedSet(new HashSet<>()); private boolean mLoaded; @@ -91,25 +96,26 @@ boolean add(KonMessage message) { } private boolean addSilent(KonMessage message) { - if (this.contains(message)) { + boolean added = mContainsSet.add(message); + if (!added) { LOGGER.warning("message already in chat: " + message); return false; } - boolean added = mSet.add(message); - return added; + mSortedSet.add(message); + return true; } public Set getAll() { this.ensureLoaded(); - return Collections.unmodifiableSet(mSet); + return Collections.unmodifiableSet(mSortedSet); } /** Get all outgoing messages with status "PENDING" for this chat. */ public SortedSet getPending() { this.ensureLoaded(); - return mSet.stream() + return mSortedSet.stream() .filter(m -> m.getStatus() == KonMessage.Status.PENDING && m instanceof OutMessage) .map(m -> (OutMessage) m) @@ -120,7 +126,7 @@ public SortedSet getPending() { public Optional getLast(String xmppID) { this.ensureLoaded(); - return mSet.descendingSet().stream() + return mSortedSet.descendingSet().stream() .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) .map(m -> (OutMessage) m).findFirst(); } @@ -128,24 +134,23 @@ public Optional getLast(String xmppID) { /** Get the last created message. */ public Optional getLast() { this.ensureLoaded(); - return mSet.isEmpty() ? + return mSortedSet.isEmpty() ? Optional.empty() : - Optional.of(mSet.last()); + Optional.of(mSortedSet.last()); } public boolean contains(KonMessage message) { this.ensureLoaded(); - // TODO ugly stupid workaround (for inconsistency of compareTo() with equal()) - return mSet.stream().anyMatch(m -> m.equals(message)); + return mContainsSet.contains(message); } public int size() { this.ensureLoaded(); - return mSet.size(); + return mSortedSet.size(); } public boolean isEmpty() { this.ensureLoaded(); - return mSet.isEmpty(); + return mSortedSet.isEmpty(); } } diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index 52ce3b42..c3ae1213 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -276,7 +276,6 @@ public boolean equals(Object o) { KonMessage oMessage = (KonMessage) o; return mChat.equals(oMessage.mChat) - && !mXMPPID.isEmpty() && mXMPPID.equals(oMessage.mXMPPID); } @@ -331,7 +330,7 @@ public static KonMessage load(ResultSet messageRS, Chat chat) throws SQLExceptio Date serverDate = sDate == 0 ? null : new Date(sDate); KonMessage.Builder builder = new KonMessage.Builder(id, chat, status, date, content); - // TODO one SQL SELECT for each message, performance? + // TODO one SQL SELECT for each message, performance? looks ok builder.transmissions(Transmission.load(id)); builder.xmppID(xmppID); if (serverDate != null) diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 9f914e37..e894bc88 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -142,7 +142,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - int hash = super.hashCode(); + int hash = 97 * super.hashCode(); return hash; } } From f9f51731b3faf597703f6aa1f24956a46fffc26a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 29 Feb 2016 17:56:11 +0100 Subject: [PATCH 173/257] model: more stream usage + streams must be synchronized --- .../java/org/kontalk/model/ContactList.java | 10 +++--- .../java/org/kontalk/model/chat/ChatList.java | 36 ++++++++----------- .../org/kontalk/model/chat/ChatMessages.java | 20 ++++++----- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 10f9617d..38be9d7d 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -126,10 +126,12 @@ public Optional getMe() { } public Set getAll(boolean withMe) { - return Collections.unmodifiableSet( - mJIDMap.values().stream() - .filter(c -> (withMe || !c.isMe())) - .collect(Collectors.toSet())); + synchronized(mJIDMap) { + return Collections.unmodifiableSet( + mJIDMap.values().stream() + .filter(c -> (withMe || !c.isMe())) + .collect(Collectors.toSet())); + } } public void delete(Contact contact) { diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 046ac493..e3ac8c2c 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -73,29 +73,24 @@ public Set getAll() { /** Get single chat with contact and XMPPID. */ public Optional get(Contact contact, String xmmpThreadID) { - for (Chat chat : mChats) { - if (!(chat instanceof SingleChat)) - continue; - SingleChat singleChat = (SingleChat) chat; - - if (singleChat.getXMPPID().equals(xmmpThreadID) - && singleChat.getContact().equals(contact)) - return Optional.of(singleChat); + synchronized(mChats) { + return mChats.stream() + .filter(chat -> chat instanceof SingleChat) + .map(chat -> (SingleChat) chat) + .filter(chat -> chat.getXMPPID().equals(xmmpThreadID) + && chat.getContact().equals(contact)) + .findFirst(); } - return Optional.empty(); } public Optional get(GroupMetaData gData) { - for (Chat chat : mChats) { - if (!(chat instanceof GroupChat)) - continue; - - GroupChat groupChat = (GroupChat) chat; - if (groupChat.getGroupData().equals(gData)) - return Optional.of(groupChat); + synchronized(mChats) { + return mChats.stream() + .filter(chat -> chat instanceof GroupChat) + .map(chat -> (GroupChat) chat) + .filter(chat -> chat.getGroupData().equals(gData)) + .findFirst(); } - - return Optional.empty(); } public Chat getOrCreate(Contact contact) { @@ -185,10 +180,9 @@ public void update(Observable o, Object arg) { return; } - for (Chat chat : mChats) { - if (!chat.isRead()) { + synchronized(mChats) { + if (mChats.stream().anyMatch(chat -> !chat.isRead())) return; - } } mUnread = false; diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 4751c0b9..998adb5f 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -115,20 +115,24 @@ public Set getAll() { public SortedSet getPending() { this.ensureLoaded(); - return mSortedSet.stream() - .filter(m -> m.getStatus() == KonMessage.Status.PENDING - && m instanceof OutMessage) - .map(m -> (OutMessage) m) - .collect(Collectors.toCollection(TreeSet::new)); + synchronized(mSortedSet) { + return mSortedSet.stream() + .filter(m -> m.getStatus() == KonMessage.Status.PENDING + && m instanceof OutMessage) + .map(m -> (OutMessage) m) + .collect(Collectors.toCollection(TreeSet::new)); + } } /** Get the newest (ie last received) outgoing message. */ public Optional getLast(String xmppID) { this.ensureLoaded(); - return mSortedSet.descendingSet().stream() - .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) - .map(m -> (OutMessage) m).findFirst(); + synchronized(mSortedSet) { + return mSortedSet.descendingSet().stream() + .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) + .map(m -> (OutMessage) m).findFirst(); + } } /** Get the last created message. */ From 80587df985180bf6726a00cee9860d64b691f8da Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 1 Mar 2016 20:59:02 +0100 Subject: [PATCH 174/257] model: fix group commands removed members --- src/main/java/org/kontalk/model/message/MessageContent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index e3ca9897..a8d25056 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -550,7 +550,7 @@ private String toJSON() { .collect(Collectors.toList()); json.put(JSON_ADDED, added); - List removed = mAdded.stream() + List removed = mRemoved.stream() .map(jid -> jid.string()) .collect(Collectors.toList()); json.put(JSON_REMOVED, removed); From c7df59a05f6295d167aa56ebf642245a93f8562a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 3 Mar 2016 16:43:07 +0100 Subject: [PATCH 175/257] client: send group chat messages using Extended Stanza Addressing (XEP-0033) --- src/main/java/org/kontalk/client/Client.java | 9 ++++++- .../org/kontalk/client/KonMessageSender.java | 26 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 8292d073..f0e00fa9 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -43,6 +43,7 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.RosterEntry; +import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.caps.EntityCapsManager; import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache; import org.jivesoftware.smackx.chatstates.ChatState; @@ -72,7 +73,7 @@ public final class Client implements StanzaListener, Runnable { private static final Map FEATURE_MAP; public enum PresenceCommand {REQUEST, GRANT, DENY}; - public enum ServerFeature {USER_AVATAR, ATTACHMENT_UPLOAD} + public enum ServerFeature {USER_AVATAR, ATTACHMENT_UPLOAD, MULTI_ADDRESSING} private enum Command {CONNECT, DISCONNECT}; @@ -88,6 +89,7 @@ private enum Command {CONNECT, DISCONNECT}; FEATURE_MAP = new HashMap<>(); FEATURE_MAP.put(PubSub.NAMESPACE, ServerFeature.USER_AVATAR); FEATURE_MAP.put(HTTPFileClient.KON_UPLOAD_FEATURE, ServerFeature.ATTACHMENT_UPLOAD); + FEATURE_MAP.put(MultipleAddresses.NAMESPACE, ServerFeature.MULTI_ADDRESSING); } private Client(Control control) { @@ -513,6 +515,11 @@ void newException(KonException konException) { mControl.onException(konException); } + String multiAddressHost() { + return mFeatures.contains(Client.ServerFeature.MULTI_ADDRESSING) + && mConn != null ? mConn.getHost() : ""; + } + @Override public void run() { while (true) { diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 227109b1..8cdcb3b1 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -19,8 +19,10 @@ package org.kontalk.client; import java.util.ArrayList; +import java.util.Set; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; @@ -110,9 +112,29 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { protoMessage.addExtension(new E2EEncryption(encryptedData)); } - // transmission specific + Set transmissions = message.getTransmissions(); + + String multiAddressHost = mClient.multiAddressHost(); + if (transmissions.size() > 1 && !multiAddressHost.isEmpty()) { + // send one message to multiple receiver using XEP-0033 + protoMessage.setTo(multiAddressHost); + MultipleAddresses addresses = new MultipleAddresses(); + for (Transmission transmission: transmissions) { + JID to = transmission.getJID(); + if (!to.isValid()) { + LOGGER.warning("invalid JID: "+to); + return false; + } + addresses.addAddress(MultipleAddresses.Type.to, to.string(), null, null, false, null); + } + protoMessage.addExtension(addresses); + + return mClient.sendPacket(protoMessage); + } + + // onle one receiver or fallback: send one message to each receiver ArrayList sendMessages = new ArrayList<>(); - for (Transmission transmission: message.getTransmissions()) { + for (Transmission transmission: transmissions) { Message sendMessage = protoMessage.clone(); JID to = transmission.getJID(); if (!to.isValid()) { From 0834225c2ebc9eb3df1566305cf987332028ef4e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 3 Mar 2016 17:50:04 +0100 Subject: [PATCH 176/257] model/database: manual commit for db delete transactions --- .../java/org/kontalk/model/chat/Chat.java | 23 ++++++------- .../org/kontalk/model/message/KonMessage.java | 12 +++++++ .../kontalk/model/message/Transmission.java | 8 +++++ .../java/org/kontalk/system/Database.java | 32 ++++++++++++------- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 55988941..f8b23865 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -37,7 +37,6 @@ import org.json.simple.JSONValue; import org.kontalk.model.Contact; import org.kontalk.model.message.KonMessage; -import org.kontalk.model.message.Transmission; import org.kontalk.system.Database; /** @@ -221,28 +220,24 @@ protected void save(List members, String subject) { void delete() { Database db = Database.getInstance(); - String whereMessages = KonMessage.COL_CHAT_ID + " == " + mID; - - // transmissions - boolean succ = db.execDeleteWhereInsecure(Transmission.TABLE, - Transmission.COL_MESSAGE_ID + " IN (SELECT _id FROM " + - KonMessage.TABLE + " WHERE " + whereMessages + ")"); - if (!succ) - return; - // messages - succ = db.execDeleteWhereInsecure(KonMessage.TABLE, whereMessages); + boolean succ = this.getMessages().getAll().stream().allMatch(m -> m.delete()); if (!succ) return; // members - boolean allDeleted = this.getAllMembers().stream() - .allMatch(m -> m.delete()); - if (!allDeleted) + succ = this.getAllMembers().stream().allMatch(m -> m.delete()); + if (!succ) return; // chat itself db.execDelete(TABLE, mID); + + // all done, commmit deletions + succ = db.commit(); + if (!succ) + return; + mDeleted = true; } diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index c3ae1213..addacc43 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -260,6 +260,18 @@ public void save() { db.execUpdate(TABLE, set, mID); } + public boolean delete() { + boolean succ = this.getTransmissions().stream().allMatch(t -> t.delete()); + if (!succ) + return false; + + if (mID < 0) { + LOGGER.warning("not in database: "+this); + return true; + } + return Database.getInstance().execDelete(TABLE, mID); + } + protected void changed(Object arg) { this.setChanged(); this.notifyObservers(arg); diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index ad59c62e..2852e07d 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -129,6 +129,14 @@ private void save() { db.execUpdate(TABLE, set, mID); } + boolean delete() { + if (mID < 0) { + LOGGER.warning("not in database: "+this); + return true; + } + return Database.getInstance().execDelete(TABLE, mID); + } + @Override public String toString() { return "T:id="+mID+",contact="+mContact+",jid="+mJID+",recdate="+mReceivedDate; diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index fffa0e43..4b4d34bd 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -94,8 +94,8 @@ private Database(Path path) throws KonException { } try { - // this is already the default - mConn.setAutoCommit(true); + // setting to false! + mConn.setAutoCommit(false); } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't set autocommit", ex); } @@ -113,6 +113,7 @@ private Database(Path path) throws KonException { try (Statement stat = mConn.createStatement()) { // set version mConn.createStatement().execute("PRAGMA "+UV+" = "+DB_VERSION); + this.commit(); this.createTable(stat, Contact.TABLE, Contact.SCHEMA); this.createTable(stat, Chat.TABLE, Chat.SCHEMA); this.createTable(stat, Member.TABLE, Member.SCHEMA); @@ -188,6 +189,7 @@ private void update(int fromVersion) throws SQLException { // set new version mConn.createStatement().execute("PRAGMA "+UV+" = "+DB_VERSION); + this.commit(); LOGGER.info("updated to version "+DB_VERSION); } @@ -195,8 +197,8 @@ synchronized void close() { try { if(mConn == null || mConn.isClosed()) return; - if (!mConn.getAutoCommit()) - mConn.commit(); + // just to be sure + mConn.commit(); mConn.close(); } catch(SQLException ex) { LOGGER.log(Level.WARNING, "can't close db", ex); @@ -253,6 +255,7 @@ public synchronized int execInsert(String table, List values) { Statement.RETURN_GENERATED_KEYS)) { insertValues(stat, values); stat.executeUpdate(); + mConn.commit(); ResultSet keys = stat.getGeneratedKeys(); return keys.getInt(1); } catch (SQLException ex) { @@ -282,6 +285,7 @@ public synchronized int execUpdate(String table, Map set, int id try (PreparedStatement stat = mConn.prepareStatement(update, Statement.RETURN_GENERATED_KEYS)) { insertValues(stat, keyList, set); stat.executeUpdate(); + mConn.commit(); ResultSet keys = stat.getGeneratedKeys(); return keys.getInt(1); } catch (SQLException ex) { @@ -290,15 +294,11 @@ public synchronized int execUpdate(String table, Map set, int id } } + /** Delete one row. Not commited! Call commit() after deletions. */ public boolean execDelete(String table, int id) { - return this.execDeleteWhereInsecure(table, "_id = " + id); - } - - public boolean execDeleteWhereInsecure(String table, String where) { - LOGGER.info("deleting from table "+table+" where "+where); + LOGGER.info("deletion, table: " + table + "; id: " + id); try (Statement stat = mConn.createStatement()) { - int c = stat.executeUpdate("DELETE FROM " + table + " WHERE " + where); - LOGGER.config("...deleted "+c+" rows"); + stat.executeUpdate("DELETE FROM " + table + " WHERE _id = " + id); } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't delete", ex); return false; @@ -306,6 +306,16 @@ public boolean execDeleteWhereInsecure(String table, String where) { return true; } + public boolean commit() { + try { + mConn.commit(); + } catch (SQLException ex) { + LOGGER.log(Level.WARNING, "can't commit", ex); + return false; + } + return true; + } + private static void insertValues(PreparedStatement stat, List keys, Map map) throws SQLException { From 896bc2cf7d55d84f3afffe9676bccc6645ade1c0 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 3 Mar 2016 18:09:44 +0100 Subject: [PATCH 177/257] model: fixing creation of sorted set without comparator --- .../java/org/kontalk/model/chat/ChatMessages.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 998adb5f..11daa879 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -21,6 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.NavigableSet; import java.util.Optional; @@ -42,13 +43,13 @@ public final class ChatMessages { private static final Logger LOGGER = Logger.getLogger(ChatMessages.class.getName()); + private static final Comparator MESSAGE_COMPARATOR = + (KonMessage o1, KonMessage o2) -> o1.getDate().compareTo(o2.getDate()); + private final Chat mChat; // comparator inconsistent with .equals(); using one set for ordering... private final NavigableSet mSortedSet = - Collections.synchronizedNavigableSet(new TreeSet( - (KonMessage o1, KonMessage o2) -> { - return o1.getDate().compareTo(o2.getDate()); } - )); + Collections.synchronizedNavigableSet(new TreeSet(MESSAGE_COMPARATOR)); // ... and one set for .contains() private final Set mContainsSet = Collections.synchronizedSet(new HashSet<>()); @@ -120,7 +121,7 @@ public SortedSet getPending() { .filter(m -> m.getStatus() == KonMessage.Status.PENDING && m instanceof OutMessage) .map(m -> (OutMessage) m) - .collect(Collectors.toCollection(TreeSet::new)); + .collect(Collectors.toCollection(() -> new TreeSet<>(MESSAGE_COMPARATOR))); } } From f3e20f9b1d26aa790f1f6d4bb7b7bada496a3c8d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 3 Mar 2016 18:44:52 +0100 Subject: [PATCH 178/257] model: fix NPE in ViewSettings.equals() --- src/main/java/org/kontalk/model/chat/Chat.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index f8b23865..bdd48d29 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -32,6 +32,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.lang.ObjectUtils; import org.jivesoftware.smackx.chatstates.ChatState; import org.json.simple.JSONObject; import org.json.simple.JSONValue; @@ -351,14 +352,17 @@ String toJSONString() { } @Override - public boolean equals(Object obj) { - if (this == obj) return true; + public boolean equals(Object o) { + if (o == this) + return true; - if (!(obj instanceof ViewSettings)) return false; + if (!(o instanceof ViewSettings)) + return false; - ViewSettings o = (ViewSettings) obj; - return mColor.equals(o.mColor) && - mImagePath.equals(o.mImagePath); + ViewSettings ovs = (ViewSettings) o; + + return ObjectUtils.equals(mColor, ovs.mColor) && + mImagePath.equals(ovs.mImagePath); } @Override From 019632e1e7f4c830986701826af509ca9f4a78e5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 3 Mar 2016 18:58:27 +0100 Subject: [PATCH 179/257] client: workaround for messages without body (and receipt request) not delivered by server --- src/main/java/org/kontalk/client/KonMessageSender.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 8cdcb3b1..542e710a 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -93,6 +93,10 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { if (!chat.isGroupChat()) protoMessage.addExtension(new DeliveryReceiptRequest()); + // TEMP: server bug workaround, always include body + if (protoMessage.getBody() == null) + protoMessage.setBody("dummy"); + if (sendChatState) protoMessage.addExtension(new ChatStateExtension(ChatState.active)); From d170ddf15237a0d4379d0385d9b503a9be8a0e63 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 4 Mar 2016 16:04:43 +0100 Subject: [PATCH 180/257] view: show new group chat after creating it --- src/main/java/org/kontalk/system/Control.java | 10 +++----- .../java/org/kontalk/view/ChatListView.java | 2 +- .../java/org/kontalk/view/ComponentUtils.java | 9 +++++-- src/main/java/org/kontalk/view/View.java | 25 ++++++++++--------- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 32fdddbd..fa0f7856 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -685,7 +685,7 @@ public Chat getOrCreateSingleChat(Contact contact) { return ChatList.getInstance().getOrCreate(contact); } - public void createGroupChat(List contacts, String subject) { + public Optional createGroupChat(List contacts, String subject) { // user is part of the group List members = contacts.stream() .map(c -> new Member(c)) @@ -693,18 +693,16 @@ public void createGroupChat(List contacts, String subject) { Contact me = ContactList.getInstance().getMe().orElse(null); if (me == null) { LOGGER.warning("can't find myself"); - return; + return Optional.empty(); } members.add(new Member(me, Member.Role.OWNER)); - KonGroupData gData = GroupControl.newKonGroupData(me.getJID()); - //MUCData gData = GroupControl.newMUCGroupData(); - GroupChat chat = ChatList.getInstance().createNew(members, - gData, + GroupControl.newKonGroupData(me.getJID()), subject); mGroupControl.getInstanceFor(chat).onCreate(); + return Optional.of(chat); } public void deleteChat(Chat chat) { diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index e9c497b8..70691a94 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -81,7 +81,7 @@ public void valueChanged(ListSelectionEvent e) { return; mView.clearSearch(); - mView.showChat(chat); + mView.selectedChatChanged(chat); lastChat = chat; } }); diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 2effdad7..a43c4a02 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -88,6 +88,7 @@ import javax.swing.text.PlainDocument; import org.kontalk.misc.JID; import org.kontalk.model.Contact; +import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; import org.kontalk.system.Config; import org.kontalk.util.Tr; @@ -369,8 +370,12 @@ private void checkSaveButton() { } private void createGroup() { - mView.getControl().createGroupChat(mList.getSelectedContacts(), - mSubjectField.getText()); + GroupChat newChat = mView.getControl().createGroupChat( + mList.getSelectedContacts(), + mSubjectField.getText()).orElse(null); + + if (newChat != null) + mView.showChat(newChat); mSubjectField.setText(""); } diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 14105ef4..c78e12d0 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -333,14 +333,25 @@ void callShutDown() { /* view internal */ void showChat(Contact contact) { - this.selectChat(mControl.getOrCreateSingleChat(contact)); + this.showChat(mControl.getOrCreateSingleChat(contact)); } - private void selectChat(Chat chat) { + void showChat(Chat chat) { + // show by selecting it mMainFrame.selectTab(MainFrame.Tab.CHATS); mChatListView.setSelectedItem(chat); } + void selectedChatChanged(Chat chat) { + if (mMainFrame.getCurrentTab() != MainFrame.Tab.CHATS) + return; + mContent.showChat(chat); + } + + void showNothing() { + mContent.showNothing(); + } + void showContactDetails(Contact contact) { if (contact.isDeleted()) return; @@ -358,16 +369,6 @@ void requestRenameFocus(Contact contact) { mContent.requestRenameFocus(); } - void showChat(Chat chat) { - if (mMainFrame.getCurrentTab() != MainFrame.Tab.CHATS) - return; - mContent.showChat(chat); - } - - void showNothing() { - mContent.showNothing(); - } - void clearSearch() { mSearchPanel.clear(); } From ccf699959f26e487120ec470c9e710f0748e9060 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 4 Mar 2016 16:18:06 +0100 Subject: [PATCH 181/257] client: don't check receiver JIDs when sending message --- .../org/kontalk/client/KonMessageSender.java | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 542e710a..25bd3ac3 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -19,8 +19,9 @@ package org.kontalk.client; import java.util.ArrayList; -import java.util.Set; +import java.util.List; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.chatstates.ChatState; @@ -34,7 +35,6 @@ import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.OutMessage; -import org.kontalk.model.message.Transmission; import org.kontalk.system.Control; import org.kontalk.util.ClientUtils; import org.kontalk.util.EncodingUtils; @@ -116,19 +116,16 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { protoMessage.addExtension(new E2EEncryption(encryptedData)); } - Set transmissions = message.getTransmissions(); + List JIDs = message.getTransmissions().stream() + .map(t -> t.getJID()) + .collect(Collectors.toList()); String multiAddressHost = mClient.multiAddressHost(); - if (transmissions.size() > 1 && !multiAddressHost.isEmpty()) { + if (JIDs.size() > 1 && !multiAddressHost.isEmpty()) { // send one message to multiple receiver using XEP-0033 protoMessage.setTo(multiAddressHost); MultipleAddresses addresses = new MultipleAddresses(); - for (Transmission transmission: transmissions) { - JID to = transmission.getJID(); - if (!to.isValid()) { - LOGGER.warning("invalid JID: "+to); - return false; - } + for (JID to: JIDs) { addresses.addAddress(MultipleAddresses.Type.to, to.string(), null, null, false, null); } protoMessage.addExtension(addresses); @@ -138,13 +135,8 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { // onle one receiver or fallback: send one message to each receiver ArrayList sendMessages = new ArrayList<>(); - for (Transmission transmission: transmissions) { + for (JID to: JIDs) { Message sendMessage = protoMessage.clone(); - JID to = transmission.getJID(); - if (!to.isValid()) { - LOGGER.warning("invalid JID: "+to); - return false; - } sendMessage.setTo(to.string()); sendMessages.add(sendMessage); } From 9d6e99f9b76fb8a39941927a93c9a02724760962 Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Tue, 1 Mar 2016 17:36:39 +0000 Subject: [PATCH 182/257] Translated using Weblate (German) Currently translated at 98.0% (253 of 258 strings) --- src/main/resources/i18n/strings_de.properties | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index b7918bb4..0deef215 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -313,3 +313,11 @@ s_T2TO=Sich selbst in den Kontakten anzeigen s_F1X0=Sich selbst in der Kontaktliste anzeigen s_3PFR=Entfernen s_D7FH=Vom Server nicht unterstützt +s_RCDY=Netzwerk +s_LPE1=Netzwerkeinstellungen +s_R4F8=Original +s_K83J=Klein (0.3MP) +s_RAMV=Mittel (0.5MP) +s_7CPG=Groß (0.8MP) +s_5RTB=Bildgröße vor dem Senden anpassen +s_13FX=Bildgröße anpassen: From 12aa7af05ba10399581d5e3011e25a5a72252169 Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Sat, 5 Mar 2016 11:02:40 +0000 Subject: [PATCH 183/257] Translated using Weblate (German) Currently translated at 98.0% (254 of 259 strings) --- src/main/resources/i18n/strings_de.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index 0deef215..b5abd402 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -321,3 +321,5 @@ s_RAMV=Mittel (0.5MP) s_7CPG=Groß (0.8MP) s_5RTB=Bildgröße vor dem Senden anpassen s_13FX=Bildgröße anpassen: +s_8OBK=Datei senden +s_7YWF=Maximalgröße: From 404fe6f885d0d98e93ac865d03b89eeeb89c4d4f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 5 Mar 2016 13:44:11 +0000 Subject: [PATCH 184/257] Translated using Weblate (German) Currently translated at 100.0% (259 of 259 strings) --- src/main/resources/i18n/strings_de.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index b5abd402..c28523a3 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -323,3 +323,8 @@ s_5RTB=Bildgröße vor dem Senden anpassen s_13FX=Bildgröße anpassen: s_8OBK=Datei senden s_7YWF=Maximalgröße: +s_4WC0=Das Server-Zertifikat konnte nicht verifiziert werden. +s_7MDX=Anfrage von Online-Status an Kontakt +s_OTEU=Automatisch Erlaubnis gewähren +s_9S4L=Erlaubnis-Anfrage +s_WUDV=Gruppenbesitzer From a976f3aa40fe95e44d6f9383b7896597007a59ee Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 5 Mar 2016 17:47:42 +0100 Subject: [PATCH 185/257] control: ignore unexpected group message of unknown groups (untested) --- src/main/java/org/kontalk/model/ContactList.java | 3 +++ .../java/org/kontalk/model/chat/ChatList.java | 2 +- .../org/kontalk/model/message/MessageContent.java | 4 ++++ .../org/kontalk/model/message/ProtoMessage.java | 6 +++++- src/main/java/org/kontalk/system/Control.java | 13 ++++++------- .../java/org/kontalk/system/GroupControl.java | 15 ++++++++++++--- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 38be9d7d..aa4f1c0d 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -118,6 +118,9 @@ public Optional get(JID jid) { */ public Optional getMe() { JID myJID = Account.getInstance().getUserJID(); + if (!myJID.isValid()) + return Optional.empty(); + Contact me = this.get(myJID).orElse(null); if (me != null) return Optional.of(me); diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index e3ac8c2c..d39b4371 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -93,7 +93,7 @@ public Optional get(GroupMetaData gData) { } } - public Chat getOrCreate(Contact contact) { + public SingleChat getOrCreate(Contact contact) { return this.getOrCreate(contact, ""); } diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index a8d25056..d2596fe8 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -530,6 +530,10 @@ public List getAdded() { return mAdded; } + public boolean isAddingMe() { + return mAdded.stream().anyMatch(jid -> jid.isMe()); + } + public List getRemoved() { return mRemoved; } diff --git a/src/main/java/org/kontalk/model/message/ProtoMessage.java b/src/main/java/org/kontalk/model/message/ProtoMessage.java index 1ede1507..425b9d76 100644 --- a/src/main/java/org/kontalk/model/message/ProtoMessage.java +++ b/src/main/java/org/kontalk/model/message/ProtoMessage.java @@ -23,7 +23,7 @@ import org.kontalk.model.Contact; /** - * An incoming message not saved to database for decryption. + * An incoming message used for decryption. Not saved to database. * * @author Alexander Bikadorov {@literal } */ @@ -83,4 +83,8 @@ public void setSecurityErrors(EnumSet errors) { mCoderStatus.setSecurityErrors(errors); } + @Override + public String toString() { + return "PM:contact="+mContact+",content="+mContent+",codstat="+mCoderStatus; + } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index fa0f7856..e1e26052 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -54,7 +54,6 @@ import org.kontalk.misc.JID; import org.kontalk.model.Avatar; import org.kontalk.model.chat.GroupChat; -import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.Member; import org.kontalk.model.message.MessageContent.Attachment; import org.kontalk.model.message.MessageContent.GroupCommand; @@ -177,20 +176,20 @@ public void onNewInMessage(MessageIDs ids, return; } - // decrypt message now to get group id + // decrypt message now to get possible group data ProtoMessage protoMessage = new ProtoMessage(sender, content); if (protoMessage.isEncrypted()) { Coder.decryptMessage(protoMessage); } // NOTE: decryption must be successful to select group chat - KonGroupData gData = protoMessage.getContent().getGroupData().orElse(null); - - Chat chat = gData != null ? - GroupControl.getGroupChat(gData, sender).orElse(null) : + Chat chat = content.getGroupData().isPresent() ? + GroupControl.getGroupChat(content, sender).orElse(null) : ChatList.getInstance().getOrCreate(sender, ids.xmppThreadID); - if (chat == null) + if (chat == null) { + LOGGER.warning("no chat found, message lost: "+protoMessage); return; + } InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, ids.xmppID, serverDate); diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 0f2119c3..fc475636 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -154,7 +154,13 @@ static KonGroupData newKonGroupData(JID myJID) { org.jivesoftware.smack.util.StringUtils.randomString(8)); } - static Optional getGroupChat(KonGroupData gData, Contact sender) { + static Optional getGroupChat(MessageContent content, Contact sender) { + KonGroupData gData = content.getGroupData().orElse(null); + if (gData == null) { + LOGGER.warning("message does not contain group data"); + return Optional.empty(); + } + ChatList chatList = ChatList.getInstance(); // get old... @@ -174,8 +180,11 @@ static Optional getGroupChat(KonGroupData gData, Contact sender) { return Optional.empty(); } - // NOTE: the message should include a CREATE or ADD group command - // if we are here (but all security checks passed so we continue) + GroupCommand command = content.getGroupCommand().orElse(null); + if (command == null || !command.isAddingMe()) { + LOGGER.warning("ignoring unexpected message of unknown group"); + return Optional.empty(); + } return Optional.of( chatList.create( From f68df8905749e652c35d589651ba7beead61b09e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 7 Mar 2016 16:59:15 +0100 Subject: [PATCH 186/257] main: shutdown if UI was requested but didn't start --- src/main/java/org/kontalk/Kontalk.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 3ee18bda..58e0e91a 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -169,7 +169,16 @@ public void run() { } }); - View view = ui ? View.create(control).orElse(null) : null; + View view; + if (ui) { + view = View.create(control).orElse(null); + if (view == null) { + control.shutDown(false); + return 5; + } + } else { + view = null; + } // order matters! ContactList.getInstance().load(); From c69cf14a03cdb714365f9931ab854f6bd8009d1a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 7 Mar 2016 17:01:12 +0100 Subject: [PATCH 187/257] model: more stream usage --- .../org/kontalk/model/message/OutMessage.java | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index e894bc88..406e01f3 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -67,26 +67,20 @@ protected OutMessage(KonMessage.Builder builder) { } public void setReceived(JID jid) { - Transmission transmission = null; - for (Transmission t: mTransmissions) { - if (t.getContact().getJID().equals(jid)) { - transmission = t; - break; - } - } - - if (transmission == null) { - LOGGER.warning("can't find transmission for received status, IDs: "+jid); - return; - } - - if (transmission.isReceived()) - // probably by another client - return; - - transmission.setReceived(new Date()); - // status only dummy value - this.changed(mStatus); + Transmission transmission = mTransmissions.stream() + .filter(t -> t.getContact().getJID().equals(jid)) + .findFirst().orElse(null); + if (transmission == null) { + LOGGER.warning("can't find transmission for received status, IDs: "+jid); + return; + } + + if (transmission.isReceived()) + // probably by another client + return; + + transmission.setReceived(new Date()); + this.changed(mStatus); } public void setStatus(Status status) { From c9e0806c29626f49935bf88b7b46163e69f9cfa5 Mon Sep 17 00:00:00 2001 From: Naofumi Date: Tue, 8 Mar 2016 11:52:15 +0000 Subject: [PATCH 188/257] Translated using Weblate (Japanese) Currently translated at 100.0% (259 of 259 strings) --- src/main/resources/i18n/strings_ja.properties | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/resources/i18n/strings_ja.properties b/src/main/resources/i18n/strings_ja.properties index 8cf55124..d2748676 100644 --- a/src/main/resources/i18n/strings_ja.properties +++ b/src/main/resources/i18n/strings_ja.properties @@ -314,3 +314,14 @@ s_T2TO=自分自身を連絡先に表示 s_F1X0=自分自身を連絡先リストに表示 s_3PFR=削除 s_D7FH=サーバーでサポートされていません +s_RCDY=ネットワーク +s_LPE1=ネットワーク設定 +s_R4F8=オリジナル +s_K83J=小 (0.3MP) +s_RAMV=中 (0.5MP) +s_7CPG=大 (0.8MP) +s_5RTB=送信前に画像のサイズを縮小します +s_13FX=添付画像のサイズを変更: +s_8OBK=ファイルを送信 +s_7YWF=最大サイズ: +s_WUDV=グループオーナー From 11c6bf4d2f36ab2ba82ae9f59c188a14d4a20a41 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Mar 2016 16:12:38 +0100 Subject: [PATCH 189/257] model: fixing NPE out of fu**ing nowhere when creating single chat I should pay more attention to leaking-'this'-warnings:| --- src/main/java/org/kontalk/model/chat/SingleChat.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index b1eeaa1c..af76512a 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -40,9 +40,9 @@ public final class SingleChat extends Chat { super(Arrays.asList(member), xmppID, "", null); mMember = member; - mMember.getContact().addObserver(this); // NOTE: Kontalk Android client is ignoring the chat XMPP-ID mXMPPID = xmppID; + mMember.getContact().addObserver(this); } // used when loading from database @@ -55,8 +55,8 @@ public final class SingleChat extends Chat { super(id, read, jsonViewSettings); mMember = member; - mMember.getContact().addObserver(this); mXMPPID = xmppID; + mMember.getContact().addObserver(this); } public Contact getContact() { @@ -136,7 +136,9 @@ public boolean equals(Object o) { if (!(o instanceof SingleChat)) return false; SingleChat oChat = (SingleChat) o; - return mMember.equals(oChat.mMember) && mXMPPID.equals(oChat.mXMPPID); + System.out.println(this+" "+oChat); + return mMember.equals(oChat.mMember) && + mXMPPID.equals(oChat.mXMPPID); } @Override From 41aeead51ab74c9c39fdc589386a4b3a3e733c1a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Mar 2016 18:45:02 +0100 Subject: [PATCH 190/257] build: set main class name for build-in gradle tasks with Netbeans --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 833b8279..554202cd 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ sourceCompatibility = '1.8' targetCompatibility = '1.8' mainClassName = 'org.kontalk.Kontalk' +ext.mainClass = mainClassName ext.clientCommonDir = 'client-common-java' applicationDefaultJvmArgs = ['''-Djava.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS|%4$-6s|%2$s-%5$s%6$s%n'''] From ddbd312af1213b9113fb1915e07623fb3b6f2e21 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 14 Mar 2016 18:48:43 +0100 Subject: [PATCH 191/257] minor code cleanup --- .../org/kontalk/client/AcknowledgedListener.java | 8 ++++---- src/main/java/org/kontalk/model/chat/GroupChat.java | 1 - src/main/java/org/kontalk/system/Control.java | 12 ++++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/kontalk/client/AcknowledgedListener.java b/src/main/java/org/kontalk/client/AcknowledgedListener.java index b5804c2b..c4d05d5d 100644 --- a/src/main/java/org/kontalk/client/AcknowledgedListener.java +++ b/src/main/java/org/kontalk/client/AcknowledgedListener.java @@ -43,8 +43,8 @@ public AcknowledgedListener(Control control) { @Override public void processPacket(Stanza p) { - // NOTE: the packet is not the acknowledgement itself but the packet that - // is acknowledged + // NOTE: the packet is not the acknowledgement itself but the packet + // that is acknowledged if (!(p instanceof Message)) { // we are only interested in acks for messages return; @@ -54,8 +54,8 @@ public void processPacket(Stanza p) { LOGGER.config("for message: "+m); if (DeliveryReceipt.from(m) != null) { - // this is an ack for a 'received' message send by - // KonMessageListener (XEP-0184), nothing must be done + // this is an ack for a 'received' message (XEP-0184) send by + // KonMessageListener, ignore return; } diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index c12d1bbf..a6e578c4 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -40,7 +40,6 @@ public abstract class GroupChat extends Chat { private static final Logger LOGGER = Logger.getLogger(GroupChat.class.getName()); - //private final HashMap mContactMap = new HashMap<>(); private final HashSet mMemberSet = new HashSet<>(); private final D mGroupData; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index e1e26052..5d3dbbd9 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -513,18 +513,18 @@ private static Optional findMessage(MessageIDs ids) { if (contact != null) { Chat chat = cl.get(contact, ids.xmppThreadID).orElse(null); if (chat != null) { - OutMessage m = chat.getMessages().getLast(ids.xmppID).orElse(null); - if (m != null) - return Optional.of(m); + Optional optM = chat.getMessages().getLast(ids.xmppID); + if (optM.isPresent()) + return optM; } } // fallback: search everywhere LOGGER.info("fallback search, IDs: "+ids); for (Chat chat: cl) { - OutMessage m = chat.getMessages().getLast(ids.xmppID).orElse(null); - if (m != null) - return Optional.of(m); + Optional optM = chat.getMessages().getLast(ids.xmppID); + if (optM.isPresent()) + return optM; } LOGGER.warning("can't find message by IDs: "+ids); From bc5e4b8ae195ed85b7006b531b6d3593ec125ea0 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 15 Mar 2016 18:53:26 +0100 Subject: [PATCH 192/257] model: started rework of model storage, unfinished --- src/main/java/org/kontalk/Kontalk.java | 9 +++-- src/main/java/org/kontalk/model/Contact.java | 29 +++++++------- .../java/org/kontalk/model/ContactList.java | 22 ++++------ .../java/org/kontalk/model/chat/Chat.java | 40 +++++++++++-------- .../java/org/kontalk/model/chat/ChatList.java | 5 ++- .../org/kontalk/model/chat/ChatMessages.java | 36 +++-------------- .../org/kontalk/model/chat/GroupChat.java | 15 ++++--- .../java/org/kontalk/model/chat/Member.java | 17 ++++---- .../org/kontalk/model/message/KonMessage.java | 36 +++++++++-------- .../kontalk/model/message/Transmission.java | 26 ++++++------ 10 files changed, 110 insertions(+), 125 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 58e0e91a..a0932c7d 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -26,6 +26,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Map; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; import java.util.logging.Handler; @@ -41,8 +42,9 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.lang.SystemUtils; import org.kontalk.crypto.PGPUtils; -import org.kontalk.model.chat.ChatList; +import org.kontalk.model.Contact; import org.kontalk.model.ContactList; +import org.kontalk.model.chat.ChatList; import org.kontalk.system.Control; import org.kontalk.util.CryptoUtils; import org.kontalk.util.EncodingUtils; @@ -181,8 +183,9 @@ public void run() { } // order matters! - ContactList.getInstance().load(); - ChatList.getInstance().load(); + // TODO move to somewhere else + Map contactMap = ContactList.getInstance().load(); + ChatList.getInstance().load(contactMap); if (view != null) view.init(); diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 0fe30b59..3eb3e931 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -20,10 +20,10 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import org.kontalk.misc.JID; import java.util.Date; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Observable; @@ -96,23 +96,24 @@ public static enum Subscription { Contact(JID jid, String name) { mJID = jid; mName = name; - Database db = Database.getInstance(); - List values = new LinkedList<>(); - values.add(mJID); - values.add(mName); - values.add(mStatus); - values.add(mLastSeen); - values.add(mEncrypted); - values.add(null); // key - values.add(null); // fingerprint - values.add(null); // avatar id - mID = db.execInsert(TABLE, values); + + // insert + List values = Arrays.asList( + mJID, + mName, + mStatus, + mLastSeen, + mEncrypted, + null, // key + null, // fingerprint + null); // avatar id + mID = Database.getInstance().execInsert(TABLE, values); if (mID < 1) LOGGER.log(Level.WARNING, "could not insert contact"); } - // used for loading contacts from database - Contact(int id, + // loading from database + public Contact(int id, JID jid, String name, String status, diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index aa4f1c0d..5508b543 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -17,9 +17,9 @@ */ package org.kontalk.model; -import org.kontalk.misc.JID; import java.sql.ResultSet; import java.sql.SQLException; +import org.kontalk.misc.JID; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -48,13 +48,12 @@ public final class ContactList extends Observable implements Iterable { /** JID to contact. Without deleted contacts. */ private final Map mJIDMap = Collections.synchronizedMap(new HashMap()); - /** Database ID to contact. With deleted contacts. */ - private final Map mIDMap = - Collections.synchronizedMap(new HashMap()); private ContactList() {} - public void load() { + public Map load() { + Map contactMap = new HashMap<>(); + Database db = Database.getInstance(); try (ResultSet resultSet = db.execSelectAll(Contact.TABLE)) { while (resultSet.next()) { @@ -68,12 +67,14 @@ public void load() { if (!contact.isDeleted()) mJIDMap.put(jid, contact); - mIDMap.put(contact.getID(), contact); + contactMap.put(contact.getID(), contact); } } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't load contacts from db", ex); } this.changed(null); + + return contactMap; } /** @@ -90,20 +91,11 @@ public Optional create(JID jid, String name) { return Optional.empty(); mJIDMap.put(newContact.getJID(), newContact); - mIDMap.put(newContact.getID(), newContact); this.changed(newContact); return Optional.of(newContact); } - public Optional get(int id) { - Contact contact = mIDMap.get(id); - if (contact == null) { - LOGGER.warning("can't find contact with ID: " + id); - } - return Optional.ofNullable(contact); - } - /** * Get the contact for a JID (if the JID is in the list). * Resource is removed for lookup. diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index bdd48d29..4fc6954a 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -22,8 +22,8 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -79,19 +79,18 @@ public abstract class Chat extends Observable implements Observer { private ViewSettings mViewSettings; protected Chat(List members, String xmppID, String subject, GroupMetaData gData) { - mMessages = new ChatMessages(this, true); + mMessages = new ChatMessages(); mRead = true; mViewSettings = new ViewSettings(); // insert - Database db = Database.getInstance(); - List values = new LinkedList<>(); - values.add(Database.setString(xmppID)); - values.add(Database.setString(subject)); - values.add(mRead); - values.add(mViewSettings.toJSONString()); - values.add(Database.setString(gData == null ? "" : gData.toJSON())); - mID = db.execInsert(TABLE, values); + List values = Arrays.asList( + Database.setString(xmppID), + Database.setString(subject), + mRead, + mViewSettings.toJSONString(), + Database.setString(gData == null ? "" : gData.toJSON())); + mID = Database.getInstance().execInsert(TABLE, values); if (mID < 1) { LOGGER.warning("couldn't insert chat"); return; @@ -103,11 +102,15 @@ protected Chat(List members, String xmppID, String subject, GroupMetaDat // used when loading from database protected Chat(int id, boolean read, String jsonViewSettings) { mID = id; - mMessages = new ChatMessages(this, false); + mMessages = new ChatMessages(); mRead = read; mViewSettings = new ViewSettings(this, jsonViewSettings); } + void loadMessages(Map contactMap) { + mMessages.load(this, contactMap); + } + public ChatMessages getMessages() { return mMessages; } @@ -256,7 +259,8 @@ public void update(Observable o, Object arg) { this.changed(o); } - static Chat loadOrNull(ResultSet rs) throws SQLException { + static Chat loadOrNull(ResultSet rs, Map contactMap) + throws SQLException { int id = rs.getInt("_id"); String jsonGD = Database.getString(rs, Chat.COL_GD); @@ -266,8 +270,8 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String xmppID = Database.getString(rs, Chat.COL_XMPPID); - // get members for chat - List members = Member.load(id); + // get members of chat + List members = Member.load(id, contactMap); String subject = Database.getString(rs, Chat.COL_SUBJ); @@ -276,15 +280,19 @@ static Chat loadOrNull(ResultSet rs) throws SQLException { String jsonViewSettings = Database.getString(rs, Chat.COL_VIEW_SET); + Chat chat; if (gData != null) { - return GroupChat.create(id, members, gData, subject, read, jsonViewSettings); + chat = GroupChat.create(id, members, gData, subject, read, jsonViewSettings); } else { if (members.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); return null; } - return new SingleChat(id, members.get(0), xmppID, read, jsonViewSettings); + chat = new SingleChat(id, members.get(0), xmppID, read, jsonViewSettings); } + + chat.loadMessages(contactMap); + return chat; } public static class ViewSettings { diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index d39b4371..8c0aaae6 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Optional; @@ -48,13 +49,13 @@ public final class ChatList extends Observable implements Observer, Iterable contactMap) { assert mChats.isEmpty(); Database db = Database.getInstance(); try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) { while (chatRS.next()) { - Chat chat = Chat.loadOrNull(chatRS); + Chat chat = Chat.loadOrNull(chatRS, contactMap); if (chat == null) continue; this.putSilent(chat); diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 11daa879..c8d8ca7b 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.Map; import java.util.NavigableSet; import java.util.Optional; import java.util.Set; @@ -31,6 +32,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import org.kontalk.model.Contact; import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.OutMessage; import org.kontalk.system.Database; @@ -46,7 +48,6 @@ public final class ChatMessages { private static final Comparator MESSAGE_COMPARATOR = (KonMessage o1, KonMessage o2) -> o1.getDate().compareTo(o2.getDate()); - private final Chat mChat; // comparator inconsistent with .equals(); using one set for ordering... private final NavigableSet mSortedSet = Collections.synchronizedNavigableSet(new TreeSet(MESSAGE_COMPARATOR)); @@ -54,29 +55,16 @@ public final class ChatMessages { private final Set mContainsSet = Collections.synchronizedSet(new HashSet<>()); - private boolean mLoaded; - - ChatMessages(Chat chat, boolean newChat) { - mChat = chat; - // don't load from db if chat is just created - mLoaded = newChat; - } - - private void ensureLoaded() { - if (mLoaded) - return; - mLoaded = true; - - this.loadMessages(); + ChatMessages() { } - private void loadMessages() { + void load(Chat chat, Map contactMap) { Database db = Database.getInstance(); try (ResultSet messageRS = db.execSelectWhereInsecure(KonMessage.TABLE, - KonMessage.COL_CHAT_ID + " == " + mChat.getID())) { + KonMessage.COL_CHAT_ID + " == " + chat.getID())) { while (messageRS.next()) { - KonMessage message = KonMessage.load(messageRS, mChat); + KonMessage message = KonMessage.load(messageRS, chat, contactMap); if (message.getTransmissions().isEmpty()) // ignore broken message continue; @@ -91,8 +79,6 @@ private void loadMessages() { * Add message to chat without notifying other components. */ boolean add(KonMessage message) { - this.ensureLoaded(); - return this.addSilent(message); } @@ -107,15 +93,11 @@ private boolean addSilent(KonMessage message) { } public Set getAll() { - this.ensureLoaded(); - return Collections.unmodifiableSet(mSortedSet); } /** Get all outgoing messages with status "PENDING" for this chat. */ public SortedSet getPending() { - this.ensureLoaded(); - synchronized(mSortedSet) { return mSortedSet.stream() .filter(m -> m.getStatus() == KonMessage.Status.PENDING @@ -127,8 +109,6 @@ public SortedSet getPending() { /** Get the newest (ie last received) outgoing message. */ public Optional getLast(String xmppID) { - this.ensureLoaded(); - synchronized(mSortedSet) { return mSortedSet.descendingSet().stream() .filter(m -> m.getXMPPID().equals(xmppID) && m instanceof OutMessage) @@ -138,24 +118,20 @@ public Optional getLast(String xmppID) { /** Get the last created message. */ public Optional getLast() { - this.ensureLoaded(); return mSortedSet.isEmpty() ? Optional.empty() : Optional.of(mSortedSet.last()); } public boolean contains(KonMessage message) { - this.ensureLoaded(); return mContainsSet.contains(message); } public int size() { - this.ensureLoaded(); return mSortedSet.size(); } public boolean isEmpty() { - this.ensureLoaded(); return mSortedSet.isEmpty(); } } diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index a6e578c4..d5fa027d 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -296,27 +296,30 @@ public String toString() { return "GC:id="+mID+",gd="+mGroupData+",subject="+mSubject; } - public static final class KonGroupChat extends GroupChat { + public static final class KonGroupChat extends GroupChat { private KonGroupChat(List members, KonGroupData gData, String subject) { super(members, gData, subject); } - private KonGroupChat(int id, List members, KonGroupData gData, String subject, boolean read, String jsonViewSettings) { + private KonGroupChat(int id, List members, KonGroupData gData, + String subject, boolean read, String jsonViewSettings) { super(id, members, gData, subject, read, jsonViewSettings); } } - public static final class MUCChat extends GroupChat { - private MUCChat(List members, GroupMetaData.MUCData gData, String subject) { + public static final class MUCChat extends GroupChat { + private MUCChat(List members, MUCData gData, String subject) { super(members, gData, subject); } - private MUCChat(int id, List members, GroupMetaData.MUCData gData, String subject, boolean read, String jsonViewSettings) { + private MUCChat(int id, List members, MUCData gData, + String subject, boolean read, String jsonViewSettings) { super(id, members, gData, subject, read, jsonViewSettings); } } - static GroupChat create(int id, List members, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { + static GroupChat create(int id, List members, + GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { return (gData instanceof KonGroupData) ? new KonGroupChat(id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : new MUCChat(id, members, (MUCData) gData, subject, read, jsonViewSettings); diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index 8b8aa22c..bf98ffbc 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -21,16 +21,16 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.system.Database; /** @@ -130,10 +130,10 @@ boolean insert(int chatID) { return true; } - List recValues = new LinkedList<>(); - recValues.add(chatID); - recValues.add(getContact().getID()); - recValues.add(mRole); + List recValues = Arrays.asList( + chatID, + getContact().getID(), + mRole); mID = Database.getInstance().execInsert(TABLE, recValues); if (mID <= 0) { LOGGER.warning("could not insert member"); @@ -161,7 +161,8 @@ protected void setState(ChatState state) { mLastActive = new Date(); } - static List load(int chatID) { + /** Load Members of a chat. */ + static List load(int chatID, Map contactMap) { Database db = Database.getInstance(); String where = COL_CHAT_ID + " == " + chatID; ResultSet resultSet; @@ -178,7 +179,7 @@ static List load(int chatID) { int contactID = resultSet.getInt(COL_CONTACT_ID); int r = resultSet.getInt(COL_ROLE); Role role = Role.values()[r]; - Contact c = ContactList.getInstance().get(contactID).orElse(null); + Contact c = contactMap.get(contactID); if (c == null) { LOGGER.warning("can't find contact, ID:"+contactID); continue; diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index addacc43..f86d1415 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -21,10 +21,10 @@ import org.kontalk.model.chat.Chat; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -37,6 +37,7 @@ import org.json.simple.JSONValue; import org.kontalk.system.Database; import org.kontalk.crypto.Coder; +import org.kontalk.model.Contact; import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.util.EncodingUtils; @@ -136,22 +137,21 @@ protected KonMessage(Chat chat, mServerError = new ServerError(); // insert - Database db = Database.getInstance(); - List values = new LinkedList<>(); - values.add(mChat.getID()); - values.add(Database.setString(mXMPPID)); - values.add(mDate); - values.add(mStatus); + List values = Arrays.asList( + mChat.getID(), + Database.setString(mXMPPID), + mDate, + mStatus, // i simply don't like to save all possible content explicitly in the // database, so we use JSON here - values.add(mContent.toJSON()); - values.add(mCoderStatus.getEncryption()); - values.add(mCoderStatus.getSigning()); - values.add(mCoderStatus.getErrors()); - values.add(mServerError.toJSON()); - values.add(mServerDate); - - mID = db.execInsert(TABLE, values); + mContent.toJSON(), + mCoderStatus.getEncryption(), + mCoderStatus.getSigning(), + mCoderStatus.getErrors(), + mServerError.toJSON(), + mServerDate); + + mID = Database.getInstance().execInsert(TABLE, values); if (mID <= 0) { LOGGER.log(Level.WARNING, "db, could not insert message"); } @@ -308,7 +308,9 @@ public String toString() { +",codstat="+mCoderStatus+",serverr="+mServerError; } - public static KonMessage load(ResultSet messageRS, Chat chat) throws SQLException { + public static KonMessage load(ResultSet messageRS, Chat chat, + Map contactMap) + throws SQLException { int id = messageRS.getInt("_id"); String xmppID = Database.getString(messageRS, KonMessage.COL_XMPP_ID); @@ -343,7 +345,7 @@ public static KonMessage load(ResultSet messageRS, Chat chat) throws SQLExceptio KonMessage.Builder builder = new KonMessage.Builder(id, chat, status, date, content); // TODO one SQL SELECT for each message, performance? looks ok - builder.transmissions(Transmission.load(id)); + builder.transmissions(Transmission.load(id, contactMap)); builder.xmppID(xmppID); if (serverDate != null) builder.serverDate(serverDate); diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index 2852e07d..122743dc 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -21,11 +21,11 @@ import org.kontalk.misc.JID; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -34,7 +34,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.system.Database; /** @@ -106,15 +105,13 @@ void setReceived(Date date) { } private int insert(int messageID) { - Database db = Database.getInstance(); - - List values = new LinkedList<>(); - values.add(messageID); - values.add(mContact.getID()); - values.add(mJID); - values.add(mReceivedDate); + List values = Arrays.asList( + messageID, + mContact.getID(), + mJID, + mReceivedDate); - int id = db.execInsert(TABLE, values); + int id = Database.getInstance().execInsert(TABLE, values); if (id <= 0) { LOGGER.log(Level.WARNING, "could not insert"); return -2; @@ -142,13 +139,13 @@ public String toString() { return "T:id="+mID+",contact="+mContact+",jid="+mJID+",recdate="+mReceivedDate; } - static Set load(int messageID) { + static Set load(int messageID, Map contactMap) { Database db = Database.getInstance(); HashSet ts = new HashSet<>(); try (ResultSet transmissionRS = db.execSelectWhereInsecure(TABLE, COL_MESSAGE_ID + " == " + messageID)) { while (transmissionRS.next()) { - ts.add(load(transmissionRS)); + ts.add(load(transmissionRS, contactMap)); } } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't load transmission(s) from db", ex); @@ -159,11 +156,12 @@ static Set load(int messageID) { return ts; } - private static Transmission load(ResultSet resultSet) throws SQLException { + private static Transmission load(ResultSet resultSet, Map contactMap) + throws SQLException { int id = resultSet.getInt("_id"); int contactID = resultSet.getInt(COL_CONTACT_ID); - Contact contact = ContactList.getInstance().get(contactID).orElse(null); + Contact contact = contactMap.get(contactID); if (contact == null) { LOGGER.warning("can't find contact in db, id: "+contactID); return null; From 57266e1bf88cdccf1685272c8cc5d59455fb091a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 16 Mar 2016 17:31:48 +0100 Subject: [PATCH 193/257] model: entities got direct reference to storage. Architecture design still not good... Selecting previous chat in view is broken. --- src/main/java/org/kontalk/Kontalk.java | 18 ++------ src/main/java/org/kontalk/model/Contact.java | 20 ++++---- .../java/org/kontalk/model/ContactList.java | 41 +++++++++++------ .../java/org/kontalk/model/chat/Chat.java | 46 +++++++++---------- .../java/org/kontalk/model/chat/ChatList.java | 37 ++++++++++----- .../org/kontalk/model/chat/ChatMessages.java | 6 +-- .../org/kontalk/model/chat/GroupChat.java | 41 +++++++++-------- .../java/org/kontalk/model/chat/Member.java | 13 +++--- .../org/kontalk/model/chat/SingleChat.java | 10 ++-- .../org/kontalk/model/message/InMessage.java | 14 +++--- .../org/kontalk/model/message/KonMessage.java | 29 ++++++------ .../org/kontalk/model/message/OutMessage.java | 12 +++-- .../kontalk/model/message/Transmission.java | 24 +++++----- .../org/kontalk/system/AvatarHandler.java | 8 ++-- src/main/java/org/kontalk/system/Control.java | 40 ++++++++++++---- .../java/org/kontalk/system/Database.java | 18 +------- 16 files changed, 209 insertions(+), 168 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index a0932c7d..04f4b7e5 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -18,15 +18,12 @@ package org.kontalk; -import org.kontalk.misc.KonException; -import org.kontalk.system.Database; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Map; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; import java.util.logging.Handler; @@ -42,9 +39,7 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.lang.SystemUtils; import org.kontalk.crypto.PGPUtils; -import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; -import org.kontalk.model.chat.ChatList; +import org.kontalk.misc.KonException; import org.kontalk.system.Control; import org.kontalk.util.CryptoUtils; import org.kontalk.util.EncodingUtils; @@ -151,16 +146,14 @@ int start(boolean ui) { // register provider PGPUtils.registerProvider(); + Control.ViewControl control; try { - // do now to test if successful - Database.ensureInitialized(); + control = Control.create(mAppDir); } catch (KonException ex) { LOGGER.log(Level.SEVERE, "can't initialize database", ex); return 5; } - final Control.ViewControl control = Control.create(); - // handle shutdown signals Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { @Override @@ -182,11 +175,6 @@ public void run() { view = null; } - // order matters! - // TODO move to somewhere else - Map contactMap = ContactList.getInstance().load(); - ChatList.getInstance().load(contactMap); - if (view != null) view.init(); diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 3eb3e931..df6e8dbe 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -78,6 +78,7 @@ public static enum Subscription { COL_AVATAR_ID + " TEXT" + ")"; + private final Database mDB; private final int mID; private JID mJID; private String mName; @@ -92,8 +93,9 @@ public static enum Subscription { //private ItemType mType; private Avatar mAvatar = null; - // used for creating new contacts (eg from roster) - Contact(JID jid, String name) { + // new contact (eg from roster) + Contact(Database db, JID jid, String name) { + mDB = db; mJID = jid; mName = name; @@ -107,13 +109,14 @@ public static enum Subscription { null, // key null, // fingerprint null); // avatar id - mID = Database.getInstance().execInsert(TABLE, values); + mID = mDB.execInsert(TABLE, values); if (mID < 1) LOGGER.log(Level.WARNING, "could not insert contact"); } // loading from database - public Contact(int id, + public Contact(Database db, + int id, JID jid, String name, String status, @@ -122,6 +125,7 @@ public Contact(int id, String publicKey, String fingerprint, String avatarID) { + mDB = db; mID = id; mJID = jid; mName = name; @@ -323,7 +327,6 @@ public boolean isDeleted() { } private void save() { - Database db = Database.getInstance(); Map set = new HashMap<>(); set.put(COL_JID, mJID); set.put(COL_NAME, mName); @@ -333,7 +336,7 @@ private void save() { set.put(COL_PUB_KEY, Database.setString(mKey)); set.put(COL_KEY_FP, Database.setString(mFingerprint)); set.put(COL_AVATAR_ID, Database.setString(mAvatar != null ? mAvatar.getID() : "")); - db.execUpdate(TABLE, set, mID); + mDB.execUpdate(TABLE, set, mID); } private void changed(Object arg) { @@ -365,7 +368,7 @@ public String toString() { +",subsc="+mSubStatus; } - static Contact load(ResultSet rs) throws SQLException { + static Contact load(Database db, ResultSet rs) throws SQLException { int id = rs.getInt("_id"); JID jid = JID.bare(rs.getString(Contact.COL_JID)); @@ -378,6 +381,7 @@ static Contact load(ResultSet rs) throws SQLException { String fp = Database.getString(rs, Contact.COL_KEY_FP); String avatarID = Database.getString(rs, Contact.COL_AVATAR_ID); - return new Contact(id, jid, name, status, Optional.ofNullable(lastSeen), encr, key, fp, avatarID); + return new Contact(db, id, jid, name, status, + Optional.ofNullable(lastSeen), encr, key, fp, avatarID); } } diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 5508b543..368f9648 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -43,21 +43,42 @@ public final class ContactList extends Observable implements Iterable { private static final Logger LOGGER = Logger.getLogger(ContactList.class.getName()); - private static final ContactList INSTANCE = new ContactList(); + private static ContactList INSTANCE = null; + private final Database mDB; /** JID to contact. Without deleted contacts. */ private final Map mJIDMap = Collections.synchronizedMap(new HashMap()); - private ContactList() {} + private ContactList(Database db) { + mDB = db; + } + + public static ContactList initialize(Database db) { + if (INSTANCE != null) { + LOGGER.warning("already initialized"); + return INSTANCE; + } + + return INSTANCE = new ContactList(db); + } + + // TODO + public static ContactList getInstance() { + if (INSTANCE == null) + throw new IllegalStateException("not initialized"); + + return INSTANCE; + } + + public Map load(Database db) { + assert mJIDMap.isEmpty(); - public Map load() { Map contactMap = new HashMap<>(); - Database db = Database.getInstance(); try (ResultSet resultSet = db.execSelectAll(Contact.TABLE)) { while (resultSet.next()) { - Contact contact = Contact.load(resultSet); + Contact contact = Contact.load(db, resultSet); JID jid = contact.getJID(); if (mJIDMap.containsKey(jid)) { @@ -77,16 +98,14 @@ public Map load() { return contactMap; } - /** - * Create and add a new contact. - */ + /** Create and add a new contact. */ public Optional create(JID jid, String name) { jid = jid.toBare(); if (!this.isValid(jid)) return Optional.empty(); - Contact newContact = new Contact(jid, name); + Contact newContact = new Contact(mDB, jid, name); if (newContact.getID() < 1) return Optional.empty(); @@ -183,8 +202,4 @@ private void changed(Object arg) { public Iterator iterator() { return mJIDMap.values().iterator(); } - - public static ContactList getInstance() { - return INSTANCE; - } } diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 4fc6954a..28492ee8 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -70,6 +70,7 @@ public abstract class Chat extends Observable implements Observer { COL_GD+" TEXT " + ")"; + private final Database mDB; protected final int mID; private final ChatMessages mMessages; @@ -78,7 +79,8 @@ public abstract class Chat extends Observable implements Observer { private ViewSettings mViewSettings; - protected Chat(List members, String xmppID, String subject, GroupMetaData gData) { + protected Chat(Database db, List members, String xmppID, String subject, GroupMetaData gData) { + mDB = db; mMessages = new ChatMessages(); mRead = true; mViewSettings = new ViewSettings(); @@ -90,25 +92,26 @@ protected Chat(List members, String xmppID, String subject, GroupMetaDat mRead, mViewSettings.toJSONString(), Database.setString(gData == null ? "" : gData.toJSON())); - mID = Database.getInstance().execInsert(TABLE, values); + mID = mDB.execInsert(TABLE, values); if (mID < 1) { LOGGER.warning("couldn't insert chat"); return; } - members.stream().forEach(member -> member.insert(mID)); + members.stream().forEach(member -> member.insert(mDB, mID)); } // used when loading from database - protected Chat(int id, boolean read, String jsonViewSettings) { + protected Chat(Database db, int id, boolean read, String jsonViewSettings) { + mDB = db; mID = id; mMessages = new ChatMessages(); mRead = read; mViewSettings = new ViewSettings(this, jsonViewSettings); } - void loadMessages(Map contactMap) { - mMessages.load(this, contactMap); + void loadMessages(Database db, Map contactMap) { + mMessages.load(db, this, contactMap); } public ChatMessages getMessages() { @@ -200,13 +203,12 @@ public boolean isGroupChat() { abstract void save(); protected void save(List members, String subject) { - Database db = Database.getInstance(); Map set = new HashMap<>(); set.put(COL_SUBJ, Database.setString(subject)); set.put(COL_READ, mRead); set.put(COL_VIEW_SET, mViewSettings.toJSONString()); - db.execUpdate(TABLE, set, mID); + mDB.execUpdate(TABLE, set, mID); // get receiver for this chat List oldMembers = new ArrayList<>(this.getAllMembers()); @@ -214,31 +216,29 @@ protected void save(List members, String subject) { // save new members members.stream() .filter(m -> !oldMembers.contains(m)) - .forEach(m -> m.insert(mID)); + .forEach(m -> m.insert(mDB, mID)); oldMembers.removeAll(members); // whats left is too much and can be deleted - oldMembers.stream().forEach(m -> m.delete()); + oldMembers.stream().forEach(m -> m.delete(mDB)); } void delete() { - Database db = Database.getInstance(); - // messages - boolean succ = this.getMessages().getAll().stream().allMatch(m -> m.delete()); + boolean succ = mMessages.getAll().stream().allMatch(m -> m.delete()); if (!succ) return; // members - succ = this.getAllMembers().stream().allMatch(m -> m.delete()); + succ = this.getAllMembers().stream().allMatch(m -> m.delete(mDB)); if (!succ) return; // chat itself - db.execDelete(TABLE, mID); + mDB.execDelete(TABLE, mID); // all done, commmit deletions - succ = db.commit(); + succ = mDB.commit(); if (!succ) return; @@ -259,7 +259,7 @@ public void update(Observable o, Object arg) { this.changed(o); } - static Chat loadOrNull(ResultSet rs, Map contactMap) + static Optional load(Database db, ResultSet rs, Map contactMap) throws SQLException { int id = rs.getInt("_id"); @@ -271,7 +271,7 @@ static Chat loadOrNull(ResultSet rs, Map contactMap) String xmppID = Database.getString(rs, Chat.COL_XMPPID); // get members of chat - List members = Member.load(id, contactMap); + List members = Member.load(db, id, contactMap); String subject = Database.getString(rs, Chat.COL_SUBJ); @@ -282,17 +282,17 @@ static Chat loadOrNull(ResultSet rs, Map contactMap) Chat chat; if (gData != null) { - chat = GroupChat.create(id, members, gData, subject, read, jsonViewSettings); + chat = GroupChat.create(db, id, members, gData, subject, read, jsonViewSettings); } else { if (members.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); - return null; + return Optional.empty(); } - chat = new SingleChat(id, members.get(0), xmppID, read, jsonViewSettings); + chat = new SingleChat(db, id, members.get(0), xmppID, read, jsonViewSettings); } - chat.loadMessages(contactMap); - return chat; + chat.loadMessages(db, contactMap); + return Optional.of(chat); } public static class ViewSettings { diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 8c0aaae6..4ae69efd 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -41,21 +41,40 @@ public final class ChatList extends Observable implements Observer, Iterable { private static final Logger LOGGER = Logger.getLogger(ChatList.class.getName()); - private static final ChatList INSTANCE = new ChatList(); + private static ChatList INSTANCE = null; + private final Database mDB; private final Set mChats = Collections.synchronizedSet(new HashSet()); private boolean mUnread = false; - private ChatList() {} + private ChatList(Database db) { + mDB = db; + } + + public static ChatList initialize(Database db) { + if (INSTANCE != null) { + LOGGER.warning("already initialized"); + return INSTANCE; + } + + return INSTANCE = new ChatList(db); + } + + // TODO + public static ChatList getInstance() { + if (INSTANCE == null) + throw new IllegalStateException("not initialized"); - public void load(Map contactMap) { + return INSTANCE; + } + + public void load(Database db, Map contactMap) { assert mChats.isEmpty(); - Database db = Database.getInstance(); try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) { while (chatRS.next()) { - Chat chat = Chat.loadOrNull(chatRS, contactMap); + Chat chat = Chat.load(db, chatRS, contactMap).orElse(null); if (chat == null) continue; this.putSilent(chat); @@ -108,7 +127,7 @@ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { } private SingleChat createNew(Contact contact, String xmppThreadID) { - SingleChat newChat = new SingleChat(new Member(contact), xmppThreadID); + SingleChat newChat = new SingleChat(mDB, new Member(contact), xmppThreadID); LOGGER.config("new single chat: "+newChat); this.putSilent(newChat); this.changed(newChat); @@ -120,7 +139,7 @@ public GroupChat create(List members, GroupMetaData gData) { } public GroupChat createNew(List members, GroupMetaData gData, String subject) { - GroupChat newChat = GroupChat.create(members, gData, subject); + GroupChat newChat = GroupChat.create(mDB, members, gData, subject); LOGGER.config("new group chat: "+newChat); this.putSilent(newChat); this.changed(newChat); @@ -194,8 +213,4 @@ public void update(Observable o, Object arg) { public Iterator iterator() { return mChats.iterator(); } - - public static ChatList getInstance() { - return INSTANCE; - } } diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index c8d8ca7b..b5590cac 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -58,13 +58,11 @@ public final class ChatMessages { ChatMessages() { } - void load(Chat chat, Map contactMap) { - Database db = Database.getInstance(); - + void load(Database db, Chat chat, Map contactMap) { try (ResultSet messageRS = db.execSelectWhereInsecure(KonMessage.TABLE, KonMessage.COL_CHAT_ID + " == " + chat.getID())) { while (messageRS.next()) { - KonMessage message = KonMessage.load(messageRS, chat, contactMap); + KonMessage message = KonMessage.load(db, messageRS, chat, contactMap); if (message.getTransmissions().isEmpty()) // ignore broken message continue; diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index d5fa027d..96706cdd 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -31,6 +31,7 @@ import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.GroupMetaData.MUCData; import org.kontalk.model.message.MessageContent; +import org.kontalk.system.Database; /** * A long-term persistent chat conversation with multiple participants. @@ -47,8 +48,8 @@ public abstract class GroupChat extends Chat { // TODO overwrite encryption=OFF field private boolean mForceEncryptionOff = false; - private GroupChat(List members, D gData, String subject) { - super(members, "", subject, gData); + private GroupChat(Database db, List members, D gData, String subject) { + super(db, members, "", subject, gData); mGroupData = gData; mSubject = subject; @@ -57,14 +58,15 @@ private GroupChat(List members, D gData, String subject) { } // used when loading from database - private GroupChat(int id, + private GroupChat(Database db, + int id, List members, D gData, String subject, boolean read, String jsonViewSettings ) { - super(id, read, jsonViewSettings); + super(db, id, read, jsonViewSettings); mGroupData = gData; mSubject = subject; @@ -297,38 +299,39 @@ public String toString() { } public static final class KonGroupChat extends GroupChat { - private KonGroupChat(List members, KonGroupData gData, String subject) { - super(members, gData, subject); + private KonGroupChat(Database db, List members, + KonGroupData gData, String subject) { + super(db, members, gData, subject); } - private KonGroupChat(int id, List members, KonGroupData gData, - String subject, boolean read, String jsonViewSettings) { - super(id, members, gData, subject, read, jsonViewSettings); + private KonGroupChat(Database db, int id, List members, + KonGroupData gData, String subject, boolean read, String jsonViewSettings) { + super(db, id, members, gData, subject, read, jsonViewSettings); } } public static final class MUCChat extends GroupChat { - private MUCChat(List members, MUCData gData, String subject) { - super(members, gData, subject); + private MUCChat(Database db, List members, MUCData gData, String subject) { + super(db, members, gData, subject); } - private MUCChat(int id, List members, MUCData gData, + private MUCChat(Database db, int id, List members, MUCData gData, String subject, boolean read, String jsonViewSettings) { - super(id, members, gData, subject, read, jsonViewSettings); + super(db, id, members, gData, subject, read, jsonViewSettings); } } - static GroupChat create(int id, List members, + static GroupChat create(Database db, int id, List members, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { return (gData instanceof KonGroupData) ? - new KonGroupChat(id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : - new MUCChat(id, members, (MUCData) gData, subject, read, jsonViewSettings); + new KonGroupChat(db, id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : + new MUCChat(db, id, members, (MUCData) gData, subject, read, jsonViewSettings); } - public static GroupChat create(List members, GroupMetaData gData, String subject) { + public static GroupChat create(Database db, List members, GroupMetaData gData, String subject) { return (gData instanceof KonGroupData) ? - new KonGroupChat(members, (KonGroupData) gData, subject) : - new MUCChat(members, (MUCData) gData, subject); + new KonGroupChat(db, members, (KonGroupData) gData, subject) : + new MUCChat(db, members, (MUCData) gData, subject); } } diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index bf98ffbc..74055b80 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -124,7 +124,7 @@ public ChatState getState() { return mState; } - boolean insert(int chatID) { + boolean insert(Database db, int chatID) { if (mID > 0) { LOGGER.warning("already in database"); return true; @@ -134,7 +134,7 @@ boolean insert(int chatID) { chatID, getContact().getID(), mRole); - mID = Database.getInstance().execInsert(TABLE, recValues); + mID = db.execInsert(TABLE, recValues); if (mID <= 0) { LOGGER.warning("could not insert member"); return false; @@ -142,17 +142,17 @@ boolean insert(int chatID) { return true; } - void save() { + void save(Database db) { // TODO } - boolean delete() { + boolean delete(Database db) { if (mID <= 0) { LOGGER.warning("not in database"); return true; } - return Database.getInstance().execDelete(TABLE, mID); + return db.execDelete(TABLE, mID); } protected void setState(ChatState state) { @@ -162,8 +162,7 @@ protected void setState(ChatState state) { } /** Load Members of a chat. */ - static List load(int chatID, Map contactMap) { - Database db = Database.getInstance(); + static List load(Database db, int chatID, Map contactMap) { String where = COL_CHAT_ID + " == " + chatID; ResultSet resultSet; try { diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index af76512a..6afcf36f 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -25,6 +25,7 @@ import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.model.Contact; +import org.kontalk.system.Database; /** * @@ -36,8 +37,8 @@ public final class SingleChat extends Chat { private final Member mMember; private final String mXMPPID; - SingleChat(Member member, String xmppID) { - super(Arrays.asList(member), xmppID, "", null); + SingleChat(Database db, Member member, String xmppID) { + super(db, Arrays.asList(member), xmppID, "", null); mMember = member; // NOTE: Kontalk Android client is ignoring the chat XMPP-ID @@ -46,13 +47,14 @@ public final class SingleChat extends Chat { } // used when loading from database - SingleChat(int id, + SingleChat(Database db, + int id, Member member, String xmppID, boolean read, String jsonViewSettings ) { - super(id, read, jsonViewSettings); + super(db, id, read, jsonViewSettings); mMember = member; mXMPPID = xmppID; diff --git a/src/main/java/org/kontalk/model/message/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java index ee546fe3..75fc579b 100644 --- a/src/main/java/org/kontalk/model/message/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -31,6 +31,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.message.MessageContent.Attachment; import org.kontalk.model.message.MessageContent.Preview; +import org.kontalk.system.Database; /** * Model for an XMPP message sent to the user. @@ -41,21 +42,22 @@ public final class InMessage extends KonMessage implements DecryptMessage { private final Transmission mTransmission; - public InMessage(ProtoMessage proto, Chat chat, JID from, String xmppID, - Optional serverDate) { - super(chat, + public InMessage(Database db, ProtoMessage proto, Chat chat, JID from, + String xmppID, Optional serverDate) { + super(db, + chat, xmppID, proto.getContent(), serverDate, Status.IN, proto.getCoderStatus()); - mTransmission = new Transmission(proto.getContact(), from, mID); + mTransmission = new Transmission(db, proto.getContact(), from, mID); } // used when loading from database - protected InMessage(KonMessage.Builder builder) { - super(builder); + protected InMessage(Database db, KonMessage.Builder builder) { + super(db, builder); if (builder.mTransmissions.size() != 1) throw new IllegalArgumentException("builder does not contain one transmission"); diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index f86d1415..55edc686 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -106,6 +106,7 @@ public static enum Status { "FOREIGN KEY ("+COL_CHAT_ID+") REFERENCES "+Chat.TABLE+" (_id) " + ")"; + private final Database mDB; protected final int mID; private final Chat mChat; private final String mXMPPID; @@ -120,12 +121,14 @@ public static enum Status { protected CoderStatus mCoderStatus; protected ServerError mServerError; - protected KonMessage(Chat chat, + protected KonMessage(Database db, + Chat chat, String xmppID, MessageContent content, Optional serverDate, Status status, CoderStatus coderStatus) { + mDB = db; mChat = chat; mXMPPID = xmppID; mDate = new Date(); @@ -151,14 +154,15 @@ protected KonMessage(Chat chat, mServerError.toJSON(), mServerDate); - mID = Database.getInstance().execInsert(TABLE, values); + mID = mDB.execInsert(TABLE, values); if (mID <= 0) { LOGGER.log(Level.WARNING, "db, could not insert message"); } } // used when loading from database - protected KonMessage(Builder builder) { + protected KonMessage(Database db, Builder builder) { + mDB = db; mID = builder.mID; mChat = builder.mChat; mXMPPID = builder.mXMPPID; @@ -247,8 +251,7 @@ public boolean isEncrypted() { return mCoderStatus.isEncrypted(); } - public void save() { - Database db = Database.getInstance(); + protected void save() { Map set = new HashMap<>(); set.put(COL_STATUS, mStatus); set.put(COL_CONTENT, mContent.toJSON()); @@ -257,7 +260,7 @@ public void save() { set.put(COL_COD_ERR, mCoderStatus.getErrors()); set.put(COL_SERV_ERR, Database.setString(mServerError.toJSON())); set.put(COL_SERV_DATE, mServerDate); - db.execUpdate(TABLE, set, mID); + mDB.execUpdate(TABLE, set, mID); } public boolean delete() { @@ -269,7 +272,7 @@ public boolean delete() { LOGGER.warning("not in database: "+this); return true; } - return Database.getInstance().execDelete(TABLE, mID); + return mDB.execDelete(TABLE, mID); } protected void changed(Object arg) { @@ -308,7 +311,7 @@ public String toString() { +",codstat="+mCoderStatus+",serverr="+mServerError; } - public static KonMessage load(ResultSet messageRS, Chat chat, + public static KonMessage load(Database db, ResultSet messageRS, Chat chat, Map contactMap) throws SQLException { int id = messageRS.getInt("_id"); @@ -345,14 +348,14 @@ public static KonMessage load(ResultSet messageRS, Chat chat, KonMessage.Builder builder = new KonMessage.Builder(id, chat, status, date, content); // TODO one SQL SELECT for each message, performance? looks ok - builder.transmissions(Transmission.load(id, contactMap)); + builder.transmissions(Transmission.load(db, id, contactMap)); builder.xmppID(xmppID); if (serverDate != null) builder.serverDate(serverDate); builder.coderStatus(coderStatus); builder.serverError(serverError); - return builder.build(); + return builder.build(db); } public static final class ServerError { @@ -426,7 +429,7 @@ private Builder(int id, private void coderStatus(CoderStatus coderStatus) { mCoderStatus = coderStatus; } private void serverError(ServerError error) { mServerError = error; } - private KonMessage build() { + private KonMessage build(Database db) { if (mTransmissions == null || mXMPPID == null || mCoderStatus == null || @@ -434,9 +437,9 @@ private KonMessage build() { throw new IllegalStateException(); if (mStatus == Status.IN) - return new InMessage(this); + return new InMessage(db, this); else - return new OutMessage(this); + return new OutMessage(db, this); } } } diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 406e01f3..407f23a4 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -30,6 +30,7 @@ import java.util.logging.Logger; import org.jivesoftware.smack.util.StringUtils; import org.kontalk.model.Contact; +import org.kontalk.system.Database; /** * Model for an XMPP message from the user to a contact. @@ -40,8 +41,9 @@ public final class OutMessage extends KonMessage { private final Set mTransmissions; - public OutMessage(Chat chat, List contacts, MessageContent content, boolean encrypted) { - super(chat, + public OutMessage(Database db, Chat chat, List contacts, + MessageContent content, boolean encrypted) { + super(db, chat, "Kon_" + StringUtils.randomString(8), content, Optional.empty(), @@ -52,7 +54,7 @@ public OutMessage(Chat chat, List contacts, MessageContent content, boo Set ts = new HashSet<>(); contacts.stream().forEach(contact -> { - boolean succ = ts.add(new Transmission(contact, contact.getJID(), mID)); + boolean succ = ts.add(new Transmission(db, contact, contact.getJID(), mID)); if (!succ) LOGGER.warning("duplicate contact: "+contact); }); @@ -60,8 +62,8 @@ public OutMessage(Chat chat, List contacts, MessageContent content, boo } // used when loading from database - protected OutMessage(KonMessage.Builder builder) { - super(builder); + protected OutMessage(Database db, KonMessage.Builder builder) { + super(db, builder); mTransmissions = Collections.unmodifiableSet(builder.mTransmissions); } diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index 122743dc..da440c57 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -62,13 +62,15 @@ final public class Transmission { "FOREIGN KEY ("+COL_CONTACT_ID+") REFERENCES "+Contact.TABLE+" (_id) " + ")"; + private final Database mDB; private final int mID; private final Contact mContact; private final JID mJID; private Date mReceivedDate; - Transmission(Contact contact, JID jid, int messageID) { + Transmission(Database db, Contact contact, JID jid, int messageID) { + mDB = db; mContact = contact; mJID = jid; mReceivedDate = null; @@ -76,7 +78,8 @@ final public class Transmission { mID = this.insert(messageID); } - private Transmission(int id, Contact contact, JID jid, Date receivedDate) { + private Transmission(Database db, int id, Contact contact, JID jid, Date receivedDate) { + mDB = db; mID = id; mContact = contact; mJID = jid; @@ -111,7 +114,7 @@ private int insert(int messageID) { mJID, mReceivedDate); - int id = Database.getInstance().execInsert(TABLE, values); + int id = mDB.execInsert(TABLE, values); if (id <= 0) { LOGGER.log(Level.WARNING, "could not insert"); return -2; @@ -120,10 +123,9 @@ private int insert(int messageID) { } private void save() { - Database db = Database.getInstance(); Map set = new HashMap<>(); set.put(COL_REC_DATE, mReceivedDate); - db.execUpdate(TABLE, set, mID); + mDB.execUpdate(TABLE, set, mID); } boolean delete() { @@ -131,7 +133,7 @@ boolean delete() { LOGGER.warning("not in database: "+this); return true; } - return Database.getInstance().execDelete(TABLE, mID); + return mDB.execDelete(TABLE, mID); } @Override @@ -139,13 +141,12 @@ public String toString() { return "T:id="+mID+",contact="+mContact+",jid="+mJID+",recdate="+mReceivedDate; } - static Set load(int messageID, Map contactMap) { - Database db = Database.getInstance(); + static Set load(Database db, int messageID, Map contactMap) { HashSet ts = new HashSet<>(); try (ResultSet transmissionRS = db.execSelectWhereInsecure(TABLE, COL_MESSAGE_ID + " == " + messageID)) { while (transmissionRS.next()) { - ts.add(load(transmissionRS, contactMap)); + ts.add(load(db, transmissionRS, contactMap)); } } catch (SQLException ex) { LOGGER.log(Level.WARNING, "can't load transmission(s) from db", ex); @@ -156,7 +157,8 @@ static Set load(int messageID, Map contactMap) { return ts; } - private static Transmission load(ResultSet resultSet, Map contactMap) + private static Transmission load(Database db, ResultSet resultSet, + Map contactMap) throws SQLException { int id = resultSet.getInt("_id"); @@ -170,7 +172,7 @@ private static Transmission load(ResultSet resultSet, Map cont long rDate = resultSet.getLong(COL_REC_DATE); Date receivedDate = rDate == 0 ? null : new Date(rDate); - return new Transmission(id, contact, jid, receivedDate); + return new Transmission(db, id, contact, jid, receivedDate); } @Override diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 7ad6243d..255abfba 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -43,9 +43,11 @@ public final class AvatarHandler { private static final int MAX_SIZE = 1024 * 250; private final Client mClient; + private final ContactList mContactList; - AvatarHandler(Client client) { + AvatarHandler(Client client, ContactList contactList) { mClient = client; + mContactList = contactList; Avatar.createDir(); } @@ -55,7 +57,7 @@ public void onNotify(JID jid, String id) { // disabled by user return; - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mContactList.get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; @@ -80,7 +82,7 @@ public void onData(JID jid, String id, byte[] avatarData) { if (avatarData.length > MAX_SIZE) LOGGER.info("avatar data too long: "+avatarData.length); - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mContactList.get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 5d3dbbd9..580f2add 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -27,6 +27,7 @@ import java.util.Date; import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.Observable; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -84,6 +85,10 @@ public enum Status { private final ViewControl mViewControl; + private final Database mDB; + private final ContactList mContactList; + private final ChatList mChatList; + private final Client mClient; private final ChatStateManager mChatStateManager; private final AttachmentManager mAttachmentManager; @@ -93,9 +98,19 @@ public enum Status { private boolean mShuttingDown = false; - private Control() { + private Control(Path appDir) throws KonException { mViewControl = new ViewControl(); + try { + mDB = new Database(appDir); + } catch (KonException ex) { + LOGGER.log(Level.SEVERE, "can't initialize database", ex); + throw ex; + } + + mContactList = ContactList.initialize(mDB); + mChatList = ChatList.initialize(mDB); + mClient = Client.create(this); mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(this); @@ -104,6 +119,10 @@ private Control() { mGroupControl = new GroupControl(this); } + public static ViewControl create(Path appDir) throws KonException { + return new Control(appDir).mViewControl; + } + public RosterHandler getRosterHandler() { return mRosterHandler; } @@ -191,7 +210,8 @@ public void onNewInMessage(MessageIDs ids, return; } - InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, + // TODO move + InMessage newMessage = new InMessage(mDB, protoMessage, chat, ids.jid, ids.xmppID, serverDate); if (newMessage.getID() <= 0) @@ -347,7 +367,8 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { return false; } - OutMessage newMessage = new OutMessage(chat, contacts, content, + // TODO move + OutMessage newMessage = new OutMessage(mDB, chat, contacts, content, chat.isSendEncrypted()); if (newMessage.getContent().getAttachment().isPresent()) mAttachmentManager.createImagePreview(newMessage); @@ -499,12 +520,6 @@ private void removeFromRoster(JID jid) { } } - /* static */ - - public static ViewControl create() { - return new Control().mViewControl; - } - private static Optional findMessage(MessageIDs ids) { ChatList cl = ChatList.getInstance(); @@ -536,6 +551,11 @@ private static Optional findMessage(MessageIDs ids) { public class ViewControl extends Observable { public void launch() { + + // order matters! + Map contactMap = mContactList.load(mDB); + mChatList.load(mDB, contactMap); + boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); if (!Account.getInstance().isPresent()) { LOGGER.info("no account found, asking for import..."); @@ -560,7 +580,7 @@ public void shutDown(boolean exit) { this.changed(new ViewEvent.StatusChange(Status.SHUTTING_DOWN, EnumSet.noneOf(Client.ServerFeature.class))); try { - Database.getInstance().close(); + mDB.close(); } catch (RuntimeException ex) { LOGGER.log(Level.WARNING, "can't close database", ex); } diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/system/Database.java index 4b4d34bd..3f027219 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/system/Database.java @@ -36,7 +36,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; -import org.kontalk.Kontalk; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; import org.kontalk.model.message.KonMessage; @@ -72,7 +71,7 @@ public final class Database { private Connection mConn = null; - private Database(Path path) throws KonException { + Database(Path appDir) throws KonException { // load the sqlite-JDBC driver using the current class loader try { Class.forName("org.sqlite.JDBC"); @@ -82,6 +81,7 @@ private Database(Path path) throws KonException { } // create database connection + Path path = appDir.resolve(FILENAME); SQLiteConfig config = new SQLiteConfig(); config.enforceForeignKeys(true); try { @@ -374,18 +374,4 @@ public static String getString(ResultSet r, String columnLabel){ public static String setString(String s) { return s.isEmpty() ? null : s; } - - public static void ensureInitialized() throws KonException { - if (INSTANCE != null) - return; - - INSTANCE = new Database(Kontalk.getInstance().appDir().resolve(Database.FILENAME)); - } - - public static Database getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("database not initialized"); - - return INSTANCE; - } } From 7524f528f19d06d93acc52897d4ec505aeb69a9b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 16 Mar 2016 20:14:41 +0100 Subject: [PATCH 194/257] refactoring everywhere. Contact and chat list not singleton anymore. Fixes selections of previous chat in view. --- src/main/java/org/kontalk/Kontalk.java | 30 +---- .../java/org/kontalk/model/ContactList.java | 27 +--- src/main/java/org/kontalk/model/Model.java | 54 ++++++++ .../java/org/kontalk/model/chat/ChatList.java | 27 +--- .../org/kontalk/model/chat/GroupChat.java | 111 +++------------- .../org/kontalk/system/AvatarHandler.java | 12 +- src/main/java/org/kontalk/system/Control.java | 125 ++++++++++-------- .../java/org/kontalk/system/GroupControl.java | 87 ++++++++---- .../org/kontalk/system/RosterHandler.java | 24 ++-- .../java/org/kontalk/view/ComponentUtils.java | 7 +- .../org/kontalk/view/ContactListView.java | 13 +- src/main/java/org/kontalk/view/MainFrame.java | 4 +- .../java/org/kontalk/view/TrayManager.java | 20 +-- src/main/java/org/kontalk/view/Utils.java | 4 +- src/main/java/org/kontalk/view/View.java | 79 ++++++----- 15 files changed, 300 insertions(+), 324 deletions(-) create mode 100644 src/main/java/org/kontalk/model/Model.java diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 04f4b7e5..5d34bbcd 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -146,39 +146,15 @@ int start(boolean ui) { // register provider PGPUtils.registerProvider(); - Control.ViewControl control; + Control control; try { control = Control.create(mAppDir); } catch (KonException ex) { - LOGGER.log(Level.SEVERE, "can't initialize database", ex); + LOGGER.log(Level.SEVERE, "can't create application", ex); return 5; } - // handle shutdown signals - Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { - @Override - public void run() { - // NOTE: logging does not work here anymore - control.shutDown(false); - System.out.println("Kontalk: shutdown finished"); - } - }); - - View view; - if (ui) { - view = View.create(control).orElse(null); - if (view == null) { - control.shutDown(false); - return 5; - } - } else { - view = null; - } - - if (view != null) - view.init(); - - control.launch(); + control.launch(ui); return 0; } diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 368f9648..7e92a2b0 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -43,42 +43,23 @@ public final class ContactList extends Observable implements Iterable { private static final Logger LOGGER = Logger.getLogger(ContactList.class.getName()); - private static ContactList INSTANCE = null; - private final Database mDB; /** JID to contact. Without deleted contacts. */ private final Map mJIDMap = Collections.synchronizedMap(new HashMap()); - private ContactList(Database db) { + ContactList(Database db) { mDB = db; } - public static ContactList initialize(Database db) { - if (INSTANCE != null) { - LOGGER.warning("already initialized"); - return INSTANCE; - } - - return INSTANCE = new ContactList(db); - } - - // TODO - public static ContactList getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("not initialized"); - - return INSTANCE; - } - - public Map load(Database db) { + Map load() { assert mJIDMap.isEmpty(); Map contactMap = new HashMap<>(); - try (ResultSet resultSet = db.execSelectAll(Contact.TABLE)) { + try (ResultSet resultSet = mDB.execSelectAll(Contact.TABLE)) { while (resultSet.next()) { - Contact contact = Contact.load(db, resultSet); + Contact contact = Contact.load(mDB, resultSet); JID jid = contact.getJID(); if (mJIDMap.containsKey(jid)) { diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java new file mode 100644 index 00000000..404700ef --- /dev/null +++ b/src/main/java/org/kontalk/model/Model.java @@ -0,0 +1,54 @@ +/* + * Kontalk Java client + * Copyright (C) 2016 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.model; + +import java.util.Map; +import java.util.logging.Logger; +import org.kontalk.model.chat.ChatList; +import org.kontalk.system.Database; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +public final class Model { + private static final Logger LOGGER = Logger.getLogger(Model.class.getName()); + + private final ContactList mContactList; + private final ChatList mChatList; + + public Model(Database db) { + mContactList = new ContactList(db); + mChatList = new ChatList(db); + } + + public ContactList contacts() { + return mContactList; + } + + public ChatList chats() { + return mChatList; + } + + public void load() { + // order matters! + Map contactMap = mContactList.load(); + mChatList.load(contactMap); + } +} diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 4ae69efd..210b5846 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -41,40 +41,21 @@ public final class ChatList extends Observable implements Observer, Iterable { private static final Logger LOGGER = Logger.getLogger(ChatList.class.getName()); - private static ChatList INSTANCE = null; - private final Database mDB; private final Set mChats = Collections.synchronizedSet(new HashSet()); private boolean mUnread = false; - private ChatList(Database db) { + public ChatList(Database db) { mDB = db; } - public static ChatList initialize(Database db) { - if (INSTANCE != null) { - LOGGER.warning("already initialized"); - return INSTANCE; - } - - return INSTANCE = new ChatList(db); - } - - // TODO - public static ChatList getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("not initialized"); - - return INSTANCE; - } - - public void load(Database db, Map contactMap) { + public void load(Map contactMap) { assert mChats.isEmpty(); - try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) { + try (ResultSet chatRS = mDB.execSelectAll(Chat.TABLE)) { while (chatRS.next()) { - Chat chat = Chat.load(db, chatRS, contactMap).orElse(null); + Chat chat = Chat.load(mDB, chatRS, contactMap).orElse(null); if (chat == null) continue; this.putSilent(chat); diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 96706cdd..bc6e92a4 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -19,18 +19,16 @@ package org.kontalk.model.chat; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.logging.Logger; import java.util.stream.Collectors; import org.jivesoftware.smackx.chatstates.ChatState; -import org.kontalk.misc.JID; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.GroupMetaData.MUCData; -import org.kontalk.model.message.MessageContent; import org.kontalk.system.Database; /** @@ -95,33 +93,9 @@ public List getValidContacts() { .collect(Collectors.toList()); } - public void addContact(Contact contact) { - this.addMemberSilent(new Member(contact)); - this.save(); - this.changed(contact); - } - - public void addContacts(List contacts) { - boolean changed = false; - for (Contact c: contacts) { - Member m = new Member(c); - if (!mMemberSet.contains(m)) { - this.addMemberSilent(m); - changed = true; - } else { - LOGGER.info("contact already in chat: "+c); - } - } - - if (changed) { - this.save(); - this.changed(contacts); - } - } - private void addMemberSilent(Member member) { if (mMemberSet.contains(member)) { - LOGGER.warning("contact already in chat: "+member); + LOGGER.warning("member already in chat: "+member); return; } @@ -129,14 +103,12 @@ private void addMemberSilent(Member member) { mMemberSet.add(member); } - private void removeContactSilent(Contact contact) { - contact.deleteObserver(this); - boolean succ = mMemberSet.remove(new Member(contact)); + private void removeMemberSilent(Member member) { + member.getContact().deleteObserver(this); + boolean succ = mMemberSet.remove(member); if (!succ) { - LOGGER.warning("contact not in chat: "+contact); - return; + LOGGER.warning("member not in chat: "+member); } - this.save(); } public D getGroupData() { @@ -172,65 +144,18 @@ public void setChatState(final Contact contact, ChatState chatState) { this.changed(member); } - public void applyGroupCommand(MessageContent.GroupCommand command, Contact sender) { - switch(command.getOperation()) { - case CREATE: - assert mMemberSet.size() == 1; - assert mMemberSet.contains(new Member(sender)); - - boolean meIn = false; - for (JID jid: command.getAdded()) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); - if (contact == null) { - LOGGER.warning("can't find contact, jid: "+jid); - continue; - } - - Member member = new Member(contact); - if (mMemberSet.contains(member)) { - LOGGER.warning("member already in chat: "+member); - continue; - } - meIn |= contact.isMe(); - this.addMemberSilent(member); - } - - if (!meIn) - LOGGER.warning("user JID not included"); - - mSubject = command.getSubject(); - this.save(); - this.changed(command); - break; - case LEAVE: - this.removeContactSilent(sender); - this.save(); - this.changed(command); - break; - case SET: - for (JID jid : command.getAdded()) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); - if (contact == null) { - LOGGER.warning("can't get added contact, jid="+jid); - continue; - } - this.addMemberSilent(new Member(contact)); - } - for (JID jid : command.getRemoved()) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); - if (contact == null) { - LOGGER.warning("can't get removed contact, jid="+jid); - continue; - } - this.removeContactSilent(contact); - } - mSubject = command.getSubject(); - this.save(); - this.changed(command); - break; - default: - LOGGER.warning("unhandled operation: "+command.getOperation()); - } + public void applyGroupChanges(List added, List removed, String subject) { + added.stream().forEach((member) -> this.addMemberSilent(member)); + removed.stream().forEach((member) -> this.removeMemberSilent(member)); + if (!subject.isEmpty()) + mSubject = subject; + + this.save(); + + if (!added.isEmpty() || !removed.isEmpty()) + this.changed(Arrays.asList()); + if (!subject.isEmpty()) + this.changed(mSubject); } @Override diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 255abfba..54cf9314 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -28,7 +28,7 @@ import org.kontalk.misc.JID; import org.kontalk.model.Avatar; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; +import org.kontalk.model.Model; import org.kontalk.util.MediaUtils; /** @@ -43,11 +43,11 @@ public final class AvatarHandler { private static final int MAX_SIZE = 1024 * 250; private final Client mClient; - private final ContactList mContactList; + private final Model mModel; - AvatarHandler(Client client, ContactList contactList) { + AvatarHandler(Client client, Model model) { mClient = client; - mContactList = contactList; + mModel = model; Avatar.createDir(); } @@ -57,7 +57,7 @@ public void onNotify(JID jid, String id) { // disabled by user return; - Contact contact = mContactList.get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; @@ -82,7 +82,7 @@ public void onData(JID jid, String id, byte[] avatarData) { if (avatarData.length > MAX_SIZE) LOGGER.info("avatar data too long: "+avatarData.length); - Contact contact = mContactList.get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid:" + jid); return; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 580f2add..6200caaa 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -27,7 +27,6 @@ import java.util.Date; import java.util.EnumSet; import java.util.List; -import java.util.Map; import java.util.Observable; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -49,11 +48,10 @@ import org.kontalk.model.chat.Chat; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.OutMessage; -import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.misc.JID; import org.kontalk.model.Avatar; +import org.kontalk.model.Model; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; import org.kontalk.model.message.MessageContent.Attachment; @@ -62,6 +60,7 @@ import org.kontalk.model.chat.SingleChat; import org.kontalk.util.ClientUtils.MessageIDs; import org.kontalk.util.XMPPUtils; +import org.kontalk.view.View; /** * Application control logic. @@ -86,10 +85,8 @@ public enum Status { private final ViewControl mViewControl; private final Database mDB; - private final ContactList mContactList; - private final ChatList mChatList; - private final Client mClient; + private final Model mModel; private final ChatStateManager mChatStateManager; private final AttachmentManager mAttachmentManager; private final RosterHandler mRosterHandler; @@ -108,19 +105,54 @@ private Control(Path appDir) throws KonException { throw ex; } - mContactList = ContactList.initialize(mDB); - mChatList = ChatList.initialize(mDB); + mModel = new Model(mDB); mClient = Client.create(this); mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(this); - mRosterHandler = new RosterHandler(this, mClient); - mAvatarHandler = new AvatarHandler(mClient); - mGroupControl = new GroupControl(this); + mRosterHandler = new RosterHandler(this, mClient, mModel); + mAvatarHandler = new AvatarHandler(mClient, mModel); + mGroupControl = new GroupControl(this, mModel); } - public static ViewControl create(Path appDir) throws KonException { - return new Control(appDir).mViewControl; + public static Control create(Path appDir) throws KonException { + Control control = new Control(appDir); + + // handle shutdown signals + Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { + @Override + public void run() { + // NOTE: logging does not work here anymore + control.mViewControl.shutDown(false); + System.out.println("Kontalk: shutdown finished"); + } + }); + + return control; + } + + public void launch(boolean ui) { + + mModel.load(); + + if (ui) { + View view = View.create(mViewControl, mModel).orElse(null); + if (view == null) { + mViewControl.shutDown(false); + return; + } + view.init(); + } + + boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); + if (!Account.getInstance().isPresent()) { + LOGGER.info("no account found, asking for import..."); + mViewControl.changed(new ViewEvent.MissingAccount(connect)); + return; + } + + if (connect) + mViewControl.connect(); } public RosterHandler getRosterHandler() { @@ -144,18 +176,18 @@ public void onStatusChange(Status status, EnumSet features String[] strings = Config.getInstance().getStringArray(Config.NET_STATUS_LIST); mClient.sendUserPresence(strings.length > 0 ? strings[0] : ""); // send all pending messages - for (Chat chat: ChatList.getInstance()) + for (Chat chat: mModel.chats()) chat.getMessages().getPending().stream() .forEach(m -> this.sendMessage(m)); // send public key requests for Kontalk contacts with missing key - for (Contact contact : ContactList.getInstance()) + for (Contact contact : mModel.contacts()) if (contact.getFingerprint().isEmpty()) this.maySendKeyRequest(contact); // TODO check current user avatar on server and upload if necessary } else if (status == Status.DISCONNECTED || status == Status.FAILED) { - for (Contact contact : ContactList.getInstance()) + for (Contact contact : mModel.contacts()) contact.setOffline(); } } @@ -203,8 +235,8 @@ public void onNewInMessage(MessageIDs ids, // NOTE: decryption must be successful to select group chat Chat chat = content.getGroupData().isPresent() ? - GroupControl.getGroupChat(content, sender).orElse(null) : - ChatList.getInstance().getOrCreate(sender, ids.xmppThreadID); + mGroupControl.getGroupChat(content, sender).orElse(null) : + mModel.chats().getOrCreate(sender, ids.xmppThreadID); if (chat == null) { LOGGER.warning("no chat found, message lost: "+protoMessage); return; @@ -243,7 +275,7 @@ public void onNewInMessage(MessageIDs ids, } public void onMessageSent(MessageIDs ids) { - OutMessage message = findMessage(ids).orElse(null); + OutMessage message = this.findMessage(ids).orElse(null); if (message == null) return; @@ -251,7 +283,7 @@ public void onMessageSent(MessageIDs ids) { } public void onMessageReceived(MessageIDs ids) { - OutMessage message = findMessage(ids).orElse(null); + OutMessage message = this.findMessage(ids).orElse(null); if (message == null) return; @@ -259,7 +291,7 @@ public void onMessageReceived(MessageIDs ids) { } public void onMessageError(MessageIDs ids, Condition condition, String errorText) { - OutMessage message = findMessage(ids).orElse(null); + OutMessage message = this.findMessage(ids).orElse(null); if (message == null) return ; message.setServerError(condition.toString(), errorText); @@ -278,13 +310,13 @@ public void onChatStateNotification(MessageIDs ids, return; } } - Contact contact = ContactList.getInstance().get(ids.jid).orElse(null); + Contact contact = mModel.contacts().get(ids.jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+ids.jid); return; } // NOTE: assume chat states are only send for single chats - SingleChat chat = ChatList.getInstance().get(contact, ids.xmppThreadID).orElse(null); + SingleChat chat = mModel.chats().get(contact, ids.xmppThreadID).orElse(null); if (chat == null) // not that important return; @@ -293,7 +325,7 @@ public void onChatStateNotification(MessageIDs ids, } public void onPGPKey(JID jid, byte[] rawKey) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.warning("can't find contact with jid: "+jid); return; @@ -337,7 +369,7 @@ public void onBlockList(JID[] jids) { } public void onContactBlocked(JID jid, boolean blocking) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("ignoring blocking of JID not in contact list"); return; @@ -408,7 +440,7 @@ void maySendKeyRequest(Contact contact) { } Optional getOrCreateContact(JID jid) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact != null) return Optional.of(contact); @@ -435,7 +467,7 @@ private Optional createContact(JID jid, String name, boolean encrypted) name = jid.local(); } - Contact newContact = ContactList.getInstance().create(jid, name).orElse(null); + Contact newContact = mModel.contacts().create(jid, name).orElse(null); if (newContact == null) { LOGGER.warning("can't create new contact"); // TODO tell view @@ -520,13 +552,11 @@ private void removeFromRoster(JID jid) { } } - private static Optional findMessage(MessageIDs ids) { - ChatList cl = ChatList.getInstance(); - + private Optional findMessage(MessageIDs ids) { // get chat by jid -> thread ID -> message id - Contact contact = ContactList.getInstance().get(ids.jid).orElse(null); + Contact contact = mModel.contacts().get(ids.jid).orElse(null); if (contact != null) { - Chat chat = cl.get(contact, ids.xmppThreadID).orElse(null); + Chat chat = mModel.chats().get(contact, ids.xmppThreadID).orElse(null); if (chat != null) { Optional optM = chat.getMessages().getLast(ids.xmppID); if (optM.isPresent()) @@ -536,7 +566,7 @@ private static Optional findMessage(MessageIDs ids) { // fallback: search everywhere LOGGER.info("fallback search, IDs: "+ids); - for (Chat chat: cl) { + for (Chat chat: mModel.chats()) { Optional optM = chat.getMessages().getLast(ids.xmppID); if (optM.isPresent()) return optM; @@ -550,23 +580,6 @@ private static Optional findMessage(MessageIDs ids) { public class ViewControl extends Observable { - public void launch() { - - // order matters! - Map contactMap = mContactList.load(mDB); - mChatList.load(mDB, contactMap); - - boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); - if (!Account.getInstance().isPresent()) { - LOGGER.info("no account found, asking for import..."); - this.changed(new ViewEvent.MissingAccount(connect)); - return; - } - - if (connect) - this.connect(); - } - public void shutDown(boolean exit) { if (mShuttingDown) // we were already here @@ -643,7 +656,7 @@ public Optional createContact(JID jid, String name, boolean encrypted) public void deleteContact(Contact contact) { JID jid = contact.getJID(); - ContactList.getInstance().delete(contact); + mModel.contacts().delete(contact); Control.this.removeFromRoster(jid); } @@ -658,7 +671,7 @@ public void changeJID(Contact contact, JID newJID) { if (oldJID.equals(newJID)) return; - boolean succ = ContactList.getInstance().changeJID(contact, newJID); + boolean succ = mModel.contacts().changeJID(contact, newJID); if (!succ) return; @@ -701,7 +714,7 @@ public void sendSubscriptionRequest(Contact contact) { /* chats */ public Chat getOrCreateSingleChat(Contact contact) { - return ChatList.getInstance().getOrCreate(contact); + return mModel.chats().getOrCreate(contact); } public Optional createGroupChat(List contacts, String subject) { @@ -709,14 +722,14 @@ public Optional createGroupChat(List contacts, String subjec List members = contacts.stream() .map(c -> new Member(c)) .collect(Collectors.toCollection(ArrayList::new)); - Contact me = ContactList.getInstance().getMe().orElse(null); + Contact me = mModel.contacts().getMe().orElse(null); if (me == null) { LOGGER.warning("can't find myself"); return Optional.empty(); } members.add(new Member(me, Member.Role.OWNER)); - GroupChat chat = ChatList.getInstance().createNew(members, + GroupChat chat = mModel.chats().createNew(members, GroupControl.newKonGroupData(me.getJID()), subject); @@ -731,7 +744,7 @@ public void deleteChat(Chat chat) { return; } - ChatList.getInstance().delete(chat); + mModel.chats().delete(chat); } public void setChatSubject(GroupChat chat, String subject) { diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index fc475636..66fba13e 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -18,14 +18,15 @@ package org.kontalk.system; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import org.kontalk.misc.JID; -import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; @@ -34,6 +35,7 @@ import org.kontalk.model.message.MessageContent.GroupCommand; /** + * Control logic for group chat management. * * @author Alexander Bikadorov {@literal } */ @@ -41,9 +43,11 @@ final class GroupControl { private static final Logger LOGGER = Logger.getLogger(GroupControl.class.getName()); private final Control mControl; + private final Model mModel; - GroupControl(Control control) { + GroupControl(Control control, Model model) { mControl = control; + mModel = model; } abstract class ChatControl { @@ -127,44 +131,76 @@ public void onInMessage(GroupCommand command, Contact sender) { } } - if (op == MessageContent.GroupCommand.OP.CREATE || - op == MessageContent.GroupCommand.OP.SET) { - // add contacts if necessary - // TODO design problem here: we need at least the public keys, but user - // might dont wanna have group members in contact list - command.getAdded().stream().forEach(jid -> { - boolean succ = mControl.getOrCreateContact(jid).isPresent(); - if (!succ) - LOGGER.warning("can't create contact, JID: "+jid); - }); + // apply group command + List added = new ArrayList<>(); + List removed = new ArrayList<>(); + String subject = ""; + + switch(command.getOperation()) { + case CREATE: + //assert mMemberSet.size() == 1; + //assert mMemberSet.contains(new Member(sender)); + added.addAll(JIDsToMembers(command.getAdded())); + if (!added.stream().anyMatch(m -> m.getContact().isMe())) + LOGGER.warning("user not included in new chat"); + + subject = command.getSubject(); + break; + case LEAVE: + removed.add(new Member(sender)); + break; + case SET: + added.addAll(JIDsToMembers(command.getAdded())); + for (JID jid : command.getRemoved()) { + Contact contact = mModel.contacts().get(jid).orElse(null); + if (contact == null) { + LOGGER.warning("can't get removed contact, jid="+jid); + continue; + } + removed.add(new Member(contact)); + } + subject = command.getSubject(); + break; + default: + LOGGER.warning("unhandled operation: "+command.getOperation()); } - mChat.applyGroupCommand(command, sender); + mChat.applyGroupChanges(added, removed, subject); } } + private List JIDsToMembers(List jids) { + List members = new ArrayList<>(); + for (JID jid: jids) { + // add contacts if necessary + // TODO design problem here: we need at least the public keys, but user + // might dont wanna have group members in contact list + Contact contact = mControl.getOrCreateContact(jid).orElse(null); + if (contact == null) { + LOGGER.warning("can't get contact, jid: "+jid); + continue; + } + members.add(new Member(contact)); + } + return members; + } + + ChatControl getInstanceFor(GroupChat chat) { if (chat instanceof KonGroupChat) return new KonChatControl((KonGroupChat) chat); throw new IllegalArgumentException("Not implemented for "+chat); } - static KonGroupData newKonGroupData(JID myJID) { - return new KonGroupData(myJID, - org.jivesoftware.smack.util.StringUtils.randomString(8)); - } - - static Optional getGroupChat(MessageContent content, Contact sender) { + Optional getGroupChat(MessageContent content, Contact sender) { KonGroupData gData = content.getGroupData().orElse(null); if (gData == null) { LOGGER.warning("message does not contain group data"); return Optional.empty(); } - ChatList chatList = ChatList.getInstance(); - // get old... - GroupChat chat = chatList.get(gData).orElse(null); + GroupChat chat = mModel.chats().get(gData).orElse(null); if (chat != null) { if (!chat.getAllContacts().contains(sender)) { LOGGER.warning("chat does not include sender: "+chat); @@ -187,8 +223,13 @@ static Optional getGroupChat(MessageContent content, Contact sender) } return Optional.of( - chatList.create( + mModel.chats().create( Arrays.asList(new Member(sender, Member.Role.OWNER)), gData)); } + + static KonGroupData newKonGroupData(JID myJID) { + return new KonGroupData(myJID, + org.jivesoftware.smack.util.StringUtils.randomString(8)); + } } diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index c2667fa0..2b4794be 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -30,8 +30,8 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.misc.ViewEvent; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; import org.kontalk.misc.JID; +import org.kontalk.model.Model; /** * Process incoming roster and presence changes. @@ -43,6 +43,7 @@ public final class RosterHandler { private final Control mControl; private final Client mClient; + private final Model mModel; private static final List KEY_SERVERS = Arrays.asList( "pgp.mit.edu" @@ -54,16 +55,17 @@ public enum Error { SERVER_NOT_FOUND } - RosterHandler(Control control, Client client) { + RosterHandler(Control control, Client client, Model model) { mControl = control; mClient = client; + mModel = model; } public void onEntryAdded(JID jid, String name, RosterPacket.ItemType type, RosterPacket.ItemStatus itemStatus) { - if (ContactList.getInstance().contains(jid)) { + if (mModel.contacts().contains(jid)) { this.onEntryUpdate(jid, name, type, itemStatus); return; } @@ -88,7 +90,7 @@ public void onEntryAdded(JID jid, public void onEntryDeleted(JID jid) { // note: also called on rename - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; @@ -101,7 +103,7 @@ public void onEntryUpdate(JID jid, String name, RosterPacket.ItemType type, RosterPacket.ItemStatus itemStatus) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; @@ -115,7 +117,7 @@ public void onEntryUpdate(JID jid, } public void onSubscriptionRequest(JID jid, byte[] rawKey) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) return; @@ -131,11 +133,11 @@ public void onSubscriptionRequest(JID jid, byte[] rawKey) { } public void onPresenceUpdate(JID jid, Presence.Type type, String status) { - if (this.isMe(jid) && !ContactList.getInstance().contains(jid)) + if (this.isMe(jid) && !mModel.contacts().contains(jid)) // don't wanna see myself return; - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; @@ -144,7 +146,7 @@ public void onPresenceUpdate(JID jid, Presence.Type type, String status) { } public void onFingerprintPresence(JID jid, String fingerprint) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; @@ -159,7 +161,7 @@ public void onFingerprintPresence(JID jid, String fingerprint) { // TODO key IDs can be forged, searching by it is defective by design public void onSignaturePresence(JID jid, String signature) { - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; @@ -214,7 +216,7 @@ public void onPresenceError(JID jid, XMPPError.Type type, XMPPError.Condition co return; } - Contact contact = ContactList.getInstance().get(jid).orElse(null); + Contact contact = mModel.contacts().get(jid).orElse(null); if (contact == null) { LOGGER.info("can't find contact with jid: "+jid); return; diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index a43c4a02..8811bde8 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -88,6 +88,7 @@ import javax.swing.text.PlainDocument; import org.kontalk.misc.JID; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; import org.kontalk.system.Config; @@ -309,13 +310,15 @@ void onShow() { static class AddGroupChatPanel extends PopupPanel { private final View mView; + private final Model mModel; private final WebTextField mSubjectField; private final ParticipantsList mList; private final WebButton mCreateButton; - AddGroupChatPanel(View view, final Component focusGainer) { + AddGroupChatPanel(View view, Model model, final Component focusGainer) { mView = view; + mModel = model; GroupPanel groupPanel = new GroupPanel(View.GAP_BIG, false); groupPanel.setMargin(View.MARGIN_BIG); @@ -383,7 +386,7 @@ private void createGroup() { @Override void onShow() { List contacts = new LinkedList<>(); - for (Contact c : Utils.allContacts()) { + for (Contact c : Utils.allContacts(mModel.contacts())) { if (c.isKontalkUser() && !c.isMe()) contacts.add(c); } diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index c826faf9..8d6fed2d 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -36,9 +36,8 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.StringEscapeUtils; -import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; +import org.kontalk.model.Model; import org.kontalk.system.Control; import org.kontalk.util.Tr; import org.kontalk.view.ContactListView.ContactItem; @@ -49,12 +48,12 @@ */ final class ContactListView extends ListView implements Observer { - private final ContactList mContactList; + private final Model mModel; - ContactListView(final View view, ContactList contactList) { + ContactListView(final View view, Model model) { super(view, true); - mContactList = contactList; + mModel = model; // actions triggered by selection this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { @@ -85,7 +84,7 @@ public void mouseClicked(MouseEvent e) { @Override protected void updateOnEDT(Object arg) { - this.sync(Utils.allContacts()); + this.sync(Utils.allContacts(mModel.contacts())); } @Override @@ -147,7 +146,7 @@ public void actionPerformed(ActionEvent event) { menu.add(deleteItem); // dont allow creation of more than one chat for a contact - newItem.setVisible(!ChatList.getInstance().contains(item.mValue)); + newItem.setVisible(!mModel.chats().contains(item.mValue)); if (item.mValue.isBlocked()) { blockItem.setVisible(false); diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 955e3a1a..8226e00e 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -56,6 +56,7 @@ import javax.swing.event.ChangeListener; import org.kontalk.system.Config; import org.kontalk.Kontalk; +import org.kontalk.model.Model; import org.kontalk.system.Control; import org.kontalk.util.Tr; @@ -75,6 +76,7 @@ static enum Tab {CHATS, CONTACT}; private final WebToggleButton mAddContactButton; MainFrame(View view, + Model model, ListView contactList, ListView chatList, Component content, @@ -212,7 +214,7 @@ public void actionPerformed(ActionEvent event) { // TODO different button Utils.getIcon("ic_ui_add.png"), Tr.tr("Create a new group chat"), - new ComponentUtils.AddGroupChatPanel(mView, this)); + new ComponentUtils.AddGroupChatPanel(mView, model, this)); WebPanel chatPanel = new GroupPanel(GroupingType.fillFirst, false, chatPane, mAddGroupButton); chatPanel.setPaintSides(false, false, false, false); diff --git a/src/main/java/org/kontalk/view/TrayManager.java b/src/main/java/org/kontalk/view/TrayManager.java index ab1c02a6..212f1855 100644 --- a/src/main/java/org/kontalk/view/TrayManager.java +++ b/src/main/java/org/kontalk/view/TrayManager.java @@ -35,7 +35,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingUtilities; -import org.kontalk.model.chat.ChatList; +import org.kontalk.model.Model; import org.kontalk.system.Config; import org.kontalk.util.Tr; @@ -50,11 +50,13 @@ final class TrayManager implements Observer { static final Image NOTIFICATION_TRAY = Utils.getImage("kontalk_notification.png"); private final View mView; + private final Model mModel; private final MainFrame mMainFrame; private TrayIcon mTrayIcon = null; - TrayManager(View view, MainFrame mainFrame) { + TrayManager(View view, Model model, MainFrame mainFrame) { mView = view; + mModel = model; mMainFrame = mainFrame; this.setTray(); } @@ -71,7 +73,7 @@ void setTray() { } if (mTrayIcon == null) - mTrayIcon = createTrayIcon(mView, mMainFrame); + mTrayIcon = this.createTrayIcon(); SystemTray tray = SystemTray.getSystemTray(); if (tray.getTrayIcons().length > 0) @@ -111,20 +113,20 @@ private void updateOnEDT(Object arg) { mTrayIcon.setImage(getTrayImage()); } - private static Image getTrayImage() { - return ChatList.getInstance().isUnread() ? + private Image getTrayImage() { + return mModel.chats().isUnread() ? NOTIFICATION_TRAY : NORMAL_TRAY ; } - private static TrayIcon createTrayIcon(final View view, final MainFrame mainFrame) { + private TrayIcon createTrayIcon() { // popup menu outside of frame, officially not supported final WebPopupMenu popup = new WebPopupMenu(); WebMenuItem quitItem = new WebMenuItem(Tr.tr("Quit")); quitItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - view.callShutDown(); + mView.callShutDown(); } }); popup.add(quitItem); @@ -143,7 +145,7 @@ public void mousePressed(MouseEvent e) { @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) - mainFrame.toggleState(); + mMainFrame.toggleState(); else check(e); } @@ -161,7 +163,7 @@ private void check(MouseEvent e) { } }; - TrayIcon trayIcon = new TrayIcon(getTrayImage(), "Kontalk" /*, popup*/); + TrayIcon trayIcon = new TrayIcon(this.getTrayImage(), "Kontalk" /*, popup*/); trayIcon.setImageAutoSize(true); trayIcon.addMouseListener(listener); return trayIcon; diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 0759a22a..16a81dbb 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -370,9 +370,9 @@ static boolean confirmDeletion(Component parent, String text) { return selectedOption == WebOptionPane.OK_OPTION; } - static Set allContacts() { + static Set allContacts(ContactList contactList) { boolean showMe = Config.getInstance().getBoolean(Config.VIEW_USER_CONTACT); - return ContactList.getInstance().getAll(showMe); + return contactList.getAll(showMe); } static List contactList(Chat chat) { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index c78e12d0..147b8236 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -47,9 +47,8 @@ import org.kontalk.system.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.chat.Chat; -import org.kontalk.model.chat.ChatList; import org.kontalk.model.Contact; -import org.kontalk.model.ContactList; +import org.kontalk.model.Model; import org.kontalk.system.Control; import org.kontalk.system.Control.ViewControl; import org.kontalk.util.EncodingUtils; @@ -92,8 +91,9 @@ public final class View implements Observer { static final Dimension AVATAR_LIST_DIM = new Dimension(30, 30); private final ViewControl mControl; - private final TrayManager mTrayManager; + private final Model mModel; + private final TrayManager mTrayManager; private final Notifier mNotifier; private final SearchPanel mSearchPanel; @@ -110,51 +110,65 @@ public final class View implements Observer { private Control.Status mCurrentStatus; private EnumSet mServerFeatures; - private View(ViewControl control) { + private View(ViewControl control, Model model) { mControl = control; + mModel = model; WebLookAndFeel.install(); - ToolTipManager.sharedInstance().setInitialDelay(200); - mContactListView = new ContactListView(this, ContactList.getInstance()); - ContactList.getInstance().addObserver(mContactListView); - mChatListView = new ChatListView(this, ChatList.getInstance()); - ChatList.getInstance().addObserver(mChatListView); - // chat view mChatView = new ChatView(this); - ChatList.getInstance().addObserver(mChatView); - // content area mContent = new Content(this, mChatView); + mContactListView = new ContactListView(this, mModel); + mChatListView = new ChatListView(this, mModel.chats()); + // search panel mSearchPanel = new SearchPanel( new ListView[]{mContactListView, mChatListView}, mChatView); - // status bar WebStatusBar statusBar = new WebStatusBar(); mStatusBarLabel = new WebStatusLabel(" "); statusBar.add(mStatusBarLabel); - // main frame - mMainFrame = new MainFrame(this, mContactListView, mChatListView, + mMainFrame = new MainFrame(this, mModel, mContactListView, mChatListView, mContent, mSearchPanel, statusBar); - mMainFrame.setVisible(true); - // tray - mTrayManager = new TrayManager(this, mMainFrame); - ChatList.getInstance().addObserver(mTrayManager); - - // hotkeys - this.setHotkeys(); - + mTrayManager = new TrayManager(this, mModel, mMainFrame); // notifier mNotifier = new Notifier(this); + // register observer + mModel.contacts().addObserver(mContactListView); + mModel.chats().addObserver(mChatListView); + mModel.chats().addObserver(mChatView); + mModel.chats().addObserver(mTrayManager); + + this.setHotkeys(); + this.statusChanged(Control.Status.DISCONNECTED, EnumSet.noneOf(Client.ServerFeature.class)); + + mMainFrame.setVisible(true); + } + + public static Optional create(ViewControl control, Model model) { + View view; + try { + view = invokeAndWait(new Callable() { + @Override + public View call() throws Exception { + return new View(control, model); + } + }); + } catch (ExecutionException | InterruptedException ex) { + LOGGER.log(Level.WARNING, "can't start view", ex); + return Optional.empty(); + } + control.addObserver(view); + return Optional.of(view); } void setHotkeys() { @@ -171,7 +185,7 @@ public void init() { public void run() { View.this.mChatListView.selectLastChat(); - if (ChatList.getInstance().isEmpty()) + if (mModel.chats().isEmpty()) mMainFrame.selectTab(MainFrame.Tab.CONTACT); } }); @@ -412,23 +426,6 @@ void updateTray() { /* static */ - public static Optional create(final ViewControl control) { - View view; - try { - view = invokeAndWait(new Callable() { - @Override - public View call() throws Exception { - return new View(control); - } - }); - } catch (ExecutionException | InterruptedException ex) { - LOGGER.log(Level.WARNING, "can't start view", ex); - return Optional.empty(); - } - control.addObserver(view); - return Optional.of(view); - } - private static T invokeAndWait(Callable callable) throws InterruptedException, ExecutionException { FutureTask task = new FutureTask<>(callable); From d4abc129f02e3fd3d144a0443dd7920550cb2b93 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 16:10:54 +0100 Subject: [PATCH 195/257] refactoring; moved message encryption from client to control --- src/main/java/org/kontalk/client/Client.java | 2 +- .../org/kontalk/client/KonMessageSender.java | 27 +++-------- src/main/java/org/kontalk/crypto/Coder.java | 30 ++++-------- .../java/org/kontalk/crypto/Decryptor.java | 15 ++---- .../java/org/kontalk/crypto/Encryptor.java | 11 ++--- src/main/java/org/kontalk/model/Account.java | 2 +- src/main/java/org/kontalk/model/Model.java | 10 +++- .../kontalk/model/message/MessageContent.java | 12 ++++- .../org/kontalk/model/message/OutMessage.java | 6 +++ .../org/kontalk/system/AttachmentManager.java | 8 +++- src/main/java/org/kontalk/system/Control.java | 47 ++++++++++++++++--- 11 files changed, 99 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index f0e00fa9..4c47f03b 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -96,7 +96,7 @@ private Client(Control control) { mControl = control; //mLimited = limited; - mMessageSender = new KonMessageSender(this, mControl); + mMessageSender = new KonMessageSender(this); // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 25bd3ac3..0603dec3 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -27,7 +27,6 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; -import org.kontalk.crypto.Coder; import org.kontalk.misc.JID; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.GroupChat.KonGroupChat; @@ -35,7 +34,6 @@ import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.OutMessage; -import org.kontalk.system.Control; import org.kontalk.util.ClientUtils; import org.kontalk.util.EncodingUtils; @@ -43,15 +41,13 @@ * * @author Alexander Bikadorov {@literal } */ -final class KonMessageSender { +public final class KonMessageSender { private static final Logger LOGGER = Logger.getLogger(KonMessageSender.class.getName()); private final Client mClient; - private final Control mControl; - KonMessageSender(Client client, Control control) { + KonMessageSender(Client client) { mClient = client; - mControl = control; } boolean sendMessage(OutMessage message, boolean sendChatState) { @@ -73,9 +69,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { return false; } - boolean encrypted = - message.getCoderStatus().getEncryption() != Coder.Encryption.NOT || - message.getCoderStatus().getSigning() != Coder.Signing.NOT; + boolean encrypted = message.isSendEncrypted(); Chat chat = message.getChat(); @@ -101,16 +95,9 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { protoMessage.addExtension(new ChatStateExtension(ChatState.active)); if (encrypted) { - byte[] encryptedData = content.isComplex() || chat.isGroupChat() ? - Coder.encryptStanza(message, - rawMessage(content, chat, true).toXML().toString()).orElse(null) : - Coder.encryptMessage(message).orElse(null); - // check also for security errors just to be sure - if (encryptedData == null || - !message.getCoderStatus().getErrors().isEmpty()) { - LOGGER.warning("encryption failed"); - message.setStatus(KonMessage.Status.ERROR); - mControl.onSecurityErrors(message); + byte[] encryptedData = content.getEncryptedData().orElse(null); + if (encryptedData == null) { + LOGGER.warning("no encrypted data"); return false; } protoMessage.addExtension(new E2EEncryption(encryptedData)); @@ -144,7 +131,7 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { return mClient.sendPackets(sendMessages.toArray(new Message[0])); } - private static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { + public static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { Message smackMessage = new Message(); // text diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index 5fb6c9bd..c0a12dd5 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -22,14 +22,12 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Optional; -import java.util.logging.Level; import java.util.logging.Logger; import org.kontalk.crypto.PGPUtils.PGPCoderKey; import org.kontalk.model.Contact; import org.kontalk.model.message.OutMessage; import org.kontalk.model.message.DecryptMessage; import org.kontalk.model.message.InMessage; -import org.kontalk.model.Account; /** * Static methods for decryption and encryption of a message. @@ -88,14 +86,6 @@ public static enum Error { private static final HashMap KEY_MAP = new HashMap<>(); - static PersonalKey myKeyOrNull() { - PersonalKey myKey = Account.getInstance().getPersonalKey().orElse(null); - if (myKey == null) - LOGGER.log(Level.WARNING, "can't get personal key"); - - return myKey; - } - public static Optional contactkey(Contact contact) { if (KEY_MAP.containsKey(contact)) { PGPCoderKey key = KEY_MAP.get(contact); @@ -120,8 +110,8 @@ public static Optional contactkey(Contact contact) { * Decrypt and verify the body of a message. Sets the encryption and signing * status of the message and errors that may occur are saved to the message. */ - public static boolean decryptMessage(DecryptMessage message) { - return new Decryptor(message).decryptMessage(); + public static boolean decryptMessage(PersonalKey myKey, DecryptMessage message) { + return new Decryptor(myKey, message).decryptMessage(); } /** @@ -129,8 +119,8 @@ public static boolean decryptMessage(DecryptMessage message) { * signing status of the message attachment and errors that may occur are * saved to the message. */ - public static void decryptAttachment(InMessage message, Path baseDir) { - new Decryptor(message).decryptAttachment(baseDir); + public static void decryptAttachment(PersonalKey myKey, InMessage message, Path baseDir) { + new Decryptor(myKey, message).decryptAttachment(baseDir); } /** @@ -138,15 +128,15 @@ public static void decryptAttachment(InMessage message, Path baseDir) { * Errors that may occur are saved to the message. * @return the encrypted and signed text. */ - public static Optional encryptMessage(OutMessage message) { - return new Encryptor(message).encryptMessage(); + public static Optional encryptMessage(PersonalKey myKey, OutMessage message) { + return new Encryptor(myKey, message).encryptMessage(); } - public static Optional encryptStanza(OutMessage message, String xml) { - return new Encryptor(message).encryptStanza(xml); + public static Optional encryptStanza(PersonalKey myKey, OutMessage message, String xml) { + return new Encryptor(myKey, message).encryptStanza(xml); } - public static Optional encryptAttachment(OutMessage message, File file) { - return new Encryptor(message).encryptAttachment(file); + public static Optional encryptAttachment(PersonalKey myKey, OutMessage message, File file) { + return new Encryptor(myKey, message).encryptAttachment(file); } } diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 0a184dd3..585dbdf0 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -74,18 +74,16 @@ private static class DecryptionResult { } private final DecryptMessage mMessage; - // nullable private final PersonalKey mMyKey; + // nullable private final PGPUtils.PGPCoderKey mSenderKey; - Decryptor(DecryptMessage message) { + Decryptor(PersonalKey myKey, DecryptMessage message) { + mMyKey = myKey; mMessage = message; - mMyKey = Coder.myKeyOrNull(); - - // if sender signing key not found -> decryption possible, signature - // verification will not be possible + // if sender signing key not found -> can decrypt but not verify mSenderKey = Coder.contactkey(message.getContact()).orElse(null); } @@ -96,11 +94,6 @@ boolean decryptMessage() { return false; } - if (mMyKey == null) { - mMessage.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); - return false; - } - // decrypt String encryptedContent = mMessage.getContent().getEncryptedContent(); if (encryptedContent.isEmpty()) { diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index fba13182..d4f4bf75 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -64,11 +64,13 @@ final class Encryptor { // should always be a power of 2 private static final int BUFFER_SIZE = 1 << 8; + private final PersonalKey myKey; private final OutMessage message; - private PersonalKey myKey = null; + private List receiverKeys = null; - Encryptor(OutMessage message) { + Encryptor(PersonalKey myKey, OutMessage message) { + this.myKey = myKey; this.message = message; } @@ -146,11 +148,6 @@ Optional encryptAttachment(File file) { } private boolean loadKeys() { - myKey = Coder.myKeyOrNull(); - if (myKey == null) { - message.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); - return false; - } List contacts = message.getTransmissions().stream() .map(t -> t.getContact()) .collect(Collectors.toList()); diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index 265a738f..d08b9b33 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -61,7 +61,7 @@ public final class Account { private PersonalKey mKey = null; - private Account(Path keyDir, Config config) { + Account(Path keyDir, Config config) { mKeyDir = keyDir; mConf = config; } diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index 404700ef..fbc7708b 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -18,9 +18,11 @@ package org.kontalk.model; +import java.nio.file.Path; import java.util.Map; import java.util.logging.Logger; import org.kontalk.model.chat.ChatList; +import org.kontalk.system.Config; import org.kontalk.system.Database; /** @@ -32,12 +34,18 @@ public final class Model { private final ContactList mContactList; private final ChatList mChatList; + private final Account mAccount; - public Model(Database db) { + public Model(Database db, Path appDir) { + mAccount = new Account(appDir, Config.getInstance()); mContactList = new ContactList(db); mChatList = new ChatList(db); } + public Account account() { + return mAccount; + } + public ContactList contacts() { return mContactList; } diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index d2596fe8..5c8e1f79 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -49,6 +49,8 @@ public class MessageContent { private final String mPlainText; // encrypted content, empty string if not present private String mEncryptedContent; + // temporary encrypted data, not saved to database + private byte[] mEncryptedData; // attachment (file url, path and metadata) private final Attachment mAttachment; // small preview file of attachment @@ -122,13 +124,21 @@ public String getEncryptedContent() { return mEncryptedContent; } - public void setDecryptedContent(MessageContent decryptedContent) { + void setDecryptedContent(MessageContent decryptedContent) { assert mDecryptedContent == null; mDecryptedContent = decryptedContent; // deleting encrypted data! mEncryptedContent = ""; } + public Optional getEncryptedData() { + return Optional.ofNullable(mEncryptedData); + } + + public void setEncryptedData(byte[] encryptedData) { + mEncryptedData = encryptedData; + } + public Optional getPreview() { if (mDecryptedContent != null && mDecryptedContent.getPreview().isPresent()) { diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 407f23a4..c238a64a 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -29,6 +29,7 @@ import java.util.Set; import java.util.logging.Logger; import org.jivesoftware.smack.util.StringUtils; +import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; import org.kontalk.system.Database; @@ -119,6 +120,11 @@ public void setUpload(URI url, String mime, long length) { this.save(); } + public boolean isSendEncrypted() { + return mCoderStatus.getEncryption() != Coder.Encryption.NOT || + mCoderStatus.getSigning() != Coder.Signing.NOT; + } + @Override public Set getTransmissions() { return mTransmissions; diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 6ac3221e..1a7658ea 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -175,7 +175,10 @@ private void uploadAsync(OutMessage message) { // if text will be encrypted, always encrypt attachment too boolean encrypt = message.getCoderStatus().getEncryption() == Encryption.DECRYPTED; if (encrypt) { - File encryptFile = Coder.encryptAttachment(message, file).orElse(null); + PersonalKey myKey = mControl.myKey().orElse(null); + File encryptFile = myKey == null ? + null : + Coder.encryptAttachment(myKey, message, file).orElse(null); if (!file.equals(original)) file.delete(); if (encryptFile == null) @@ -255,7 +258,8 @@ public void updateProgress(int p) { // decrypt file if (attachment.getCoderStatus().isEncrypted()) { - Coder.decryptAttachment(message, mAttachmentDir); + mControl.myKey().ifPresent(mk -> + Coder.decryptAttachment(mk, message, mAttachmentDir)); } // create preview if not in message diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 6200caaa..73ca7d7a 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -37,6 +37,7 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.Kontalk; import org.kontalk.client.Client; +import org.kontalk.client.KonMessageSender; import org.kontalk.crypto.Coder; import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PGPUtils.PGPCoderKey; @@ -105,7 +106,7 @@ private Control(Path appDir) throws KonException { throw ex; } - mModel = new Model(mDB); + mModel = new Model(mDB, appDir); mClient = Client.create(this); mChatStateManager = new ChatStateManager(mClient); @@ -145,7 +146,7 @@ public void launch(boolean ui) { } boolean connect = Config.getInstance().getBoolean(Config.MAIN_CONNECT_STARTUP); - if (!Account.getInstance().isPresent()) { + if (!mModel.account().isPresent()) { LOGGER.info("no account found, asking for import..."); mViewControl.changed(new ViewEvent.MissingAccount(connect)); return; @@ -230,7 +231,7 @@ public void onNewInMessage(MessageIDs ids, // decrypt message now to get possible group data ProtoMessage protoMessage = new ProtoMessage(sender, content); if (protoMessage.isEncrypted()) { - Coder.decryptMessage(protoMessage); + this.myKey().ifPresent(mk -> Coder.decryptMessage(mk, protoMessage)); } // NOTE: decryption must be successful to select group chat @@ -413,13 +414,38 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { } boolean sendMessage(OutMessage message) { - if (message.getContent().getAttachment().isPresent() && - !message.getContent().getAttachment().get().hasURL()) { + MessageContent content = message.getContent(); + if (content.getAttachment().isPresent() && + !content.getAttachment().get().hasURL()) { // continue later... mAttachmentManager.queueUpload(message); return false; } + // prepare encrypted content + if (message.isSendEncrypted()) { + PersonalKey myKey = this.myKey().orElse(null); + if (myKey == null) + return false; + + Chat chat = message.getChat(); + byte[] encryptedData; + if (content.isComplex() || chat.isGroupChat()) { + String stanza = KonMessageSender.rawMessage(content, chat, true).toXML().toString(); + encryptedData = Coder.encryptStanza(myKey, message, stanza).orElse(null); + } else { + encryptedData = Coder.encryptMessage(myKey, message).orElse(null); + } + // check also for security errors just to be sure + if (encryptedData == null || !message.getCoderStatus().getErrors().isEmpty()) { + LOGGER.warning("encryption failed"); + message.setStatus(KonMessage.Status.ERROR); + this.onSecurityErrors(message); + return false; + } + content.setEncryptedData(encryptedData); + } + boolean sent = mClient.sendMessage(message, Config.getInstance().getBoolean(Config.NET_SEND_CHAT_STATE)); mChatStateManager.handleOwnChatStateEvent(message.getChat(), ChatState.active); @@ -455,6 +481,14 @@ void sendPresenceSubscription(JID jid, Client.PresenceCommand command) { mClient.sendPresenceSubscription(jid, command); } + Optional myKey() { + Optional myKey = mModel.account().getPersonalKey(); + if (!myKey.isPresent()) { + LOGGER.log(Level.WARNING, "can't get personal key"); + } + return myKey; + } + /* private */ private Optional createContact(JID jid, String name, boolean encrypted) { @@ -487,13 +521,12 @@ private void decryptAndProcess(InMessage message) { if (!message.isEncrypted()) { LOGGER.info("message not encrypted"); } else { - Coder.decryptMessage(message); + this.myKey().ifPresent(mk -> Coder.decryptMessage(mk, message)); } this.processContent(message); } - private void setKey(Contact contact, PGPCoderKey key) { contact.setKey(key.rawKey, key.fingerprint); From 425b5494547065669d837614b7ead95b03382236 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 16:22:48 +0100 Subject: [PATCH 196/257] misc: removed usage of model in jid class --- src/main/java/org/kontalk/misc/JID.java | 6 ------ src/main/java/org/kontalk/model/Contact.java | 2 +- src/main/java/org/kontalk/model/message/MessageContent.java | 4 +++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 1bef9024..48f1cca2 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -23,7 +23,6 @@ import org.jxmpp.jid.util.JidUtil; import org.jxmpp.stringprep.simple.SimpleXmppStringprep; import org.jxmpp.util.XmppStringUtils; -import org.kontalk.model.Account; /** * A Jabber ID (the address of an XMPP entity). Immutable. @@ -82,11 +81,6 @@ public boolean isFull() { return !mResource.isEmpty(); } - public boolean isMe() { - return this.isValid() && - this.equals(Account.getInstance().getUserJID()); - } - public JID toBare() { return new JID(mLocal, mDomain, ""); } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index df6e8dbe..ae85a9cd 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -295,7 +295,7 @@ public void deleteAvatar() { } public boolean isMe() { - return mJID.isMe(); + return mJID.isValid() && mJID.equals(Account.getUserJID()); } public boolean isKontalkUser(){ diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index 5c8e1f79..b49461fd 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -33,6 +33,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.crypto.Coder; +import org.kontalk.model.Account; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.util.EncodingUtils; @@ -541,7 +542,8 @@ public List getAdded() { } public boolean isAddingMe() { - return mAdded.stream().anyMatch(jid -> jid.isMe()); + JID myJID = Account.getUserJID(); + return mAdded.stream().anyMatch(jid -> jid.equals(myJID)); } public List getRemoved() { From 3925c087b32f87a119d0cbb0c3e98536e7e72dc4 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 16:53:00 +0100 Subject: [PATCH 197/257] model: refactoring; account is not singleton anymore --- src/main/java/org/kontalk/client/Client.java | 2 +- .../kontalk/client/KonConnectionListener.java | 8 +++-- src/main/java/org/kontalk/model/Account.java | 12 ++----- .../java/org/kontalk/model/ContactList.java | 2 +- .../org/kontalk/system/AccountImporter.java | 9 +++--- .../org/kontalk/system/AttachmentManager.java | 27 +++++++--------- src/main/java/org/kontalk/system/Control.java | 15 ++++++++- .../org/kontalk/view/ConfigurationDialog.java | 32 ++++++++++++------- .../java/org/kontalk/view/ImportDialog.java | 6 ++-- src/main/java/org/kontalk/view/View.java | 2 +- 10 files changed, 64 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 4c47f03b..d25651cb 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -148,7 +148,7 @@ public void connect(PersonalKey key) { validateCertificate); // connection listener - mConn.addConnectionListener(new KonConnectionListener(this)); + mConn.addConnectionListener(new KonConnectionListener(this, mControl)); Roster roster = Roster.getInstanceFor(mConn); // subscriptions handled by roster handler diff --git a/src/main/java/org/kontalk/client/KonConnectionListener.java b/src/main/java/org/kontalk/client/KonConnectionListener.java index b622ccee..626bd68c 100644 --- a/src/main/java/org/kontalk/client/KonConnectionListener.java +++ b/src/main/java/org/kontalk/client/KonConnectionListener.java @@ -24,7 +24,6 @@ import org.jivesoftware.smack.XMPPConnection; import org.kontalk.misc.JID; import org.kontalk.misc.KonException; -import org.kontalk.model.Account; import org.kontalk.system.Control; /** @@ -35,10 +34,13 @@ final class KonConnectionListener implements ConnectionListener { private static final Logger LOGGER = Logger.getLogger(KonConnectionListener.class.getName()); private final Client mClient; + private final Control mControl; + private boolean mConnected = false; - KonConnectionListener(Client client) { + KonConnectionListener(Client client, Control control) { mClient = client; + mControl = control; } @Override @@ -51,7 +53,7 @@ public void connected(XMPPConnection connection) { public void authenticated(XMPPConnection connection, boolean resumed) { JID jid = JID.bare(connection.getUser()); LOGGER.info("as "+jid); - Account.getInstance().setJID(jid); + mControl.onAuthenticated(jid); } @Override diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index d08b9b33..0b2ef3de 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -35,7 +35,6 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; import org.jivesoftware.smack.util.StringUtils; -import org.kontalk.Kontalk; import org.kontalk.misc.KonException; import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; @@ -133,10 +132,6 @@ public void setJID(JID jid) { Config.getInstance().setProperty(Config.ACC_JID, jid.string()); } - public JID getUserJID() { - return JID.bare(Config.getInstance().getString(Config.ACC_JID)); - } - private void writePrivateKey(byte[] privateKeyData, char[] oldPassword, char[] newPassword) @@ -203,10 +198,7 @@ private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } - public synchronized static Account getInstance() { - if (INSTANCE == null) { - INSTANCE = new Account(Kontalk.getInstance().appDir(), Config.getInstance()); - } - return INSTANCE; + public static JID getUserJID() { + return JID.bare(Config.getInstance().getString(Config.ACC_JID)); } } diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 7e92a2b0..f825943a 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -109,7 +109,7 @@ public Optional get(JID jid) { * if not yet in the list. */ public Optional getMe() { - JID myJID = Account.getInstance().getUserJID(); + JID myJID = Account.getUserJID(); if (!myJID.isValid()) return Optional.empty(); diff --git a/src/main/java/org/kontalk/system/AccountImporter.java b/src/main/java/org/kontalk/system/AccountImporter.java index 6eb4b02c..81bab432 100644 --- a/src/main/java/org/kontalk/system/AccountImporter.java +++ b/src/main/java/org/kontalk/system/AccountImporter.java @@ -21,7 +21,6 @@ import org.kontalk.model.Account; import java.io.IOException; import java.util.Observable; -import java.util.Observer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; @@ -42,11 +41,13 @@ public final class AccountImporter extends Observable implements Callback.Handle static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; + private final Account mAccount; + private char[] mPassword = null; private boolean mAborted = false; - public AccountImporter(Observer o) { - this.addObserver(o); + AccountImporter(Account account) { + mAccount = account; } public void fromZipFile(String zipFilePath, char[] password) { @@ -111,7 +112,7 @@ public void handle(Callback callback) { private void set(byte[] privateKeyData, char[] password) { try { - Account.getInstance().setAccount(privateKeyData, password); + mAccount.setAccount(privateKeyData, password); } catch (KonException ex) { this.changed(ex); return; diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 1a7658ea..e8f49948 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -18,7 +18,6 @@ package org.kontalk.system; -import org.kontalk.model.Account; import java.awt.Dimension; import java.awt.Image; import java.awt.image.BufferedImage; @@ -188,7 +187,7 @@ private void uploadAsync(OutMessage message) { //mime = "application/octet-stream"; } - HTTPFileClient client = createClientOrNull(); + HTTPFileClient client = this.clientOrNull(); if (client == null) return; @@ -227,7 +226,7 @@ private void downloadAsync(final InMessage message) { return; } - HTTPFileClient client = createClientOrNull(); + HTTPFileClient client = this.clientOrNull(); if (client == null) return; @@ -351,6 +350,16 @@ private void writePreview(Preview preview, String filename) { LOGGER.info("to file: "+newFile); } + private HTTPFileClient clientOrNull(){ + PersonalKey key = mControl.myKey().orElse(null); + if (key == null) + return null; + + return new HTTPFileClient(key.getServerLoginKey(), + key.getBridgeCertificate(), + Config.getInstance().getBoolean(Config.SERV_CERT_VALIDATION)); + } + @Override public void run() { while (true) { @@ -402,16 +411,4 @@ static Attachment attachmentOrNull(Path path) { } return new Attachment(path, mimeType); } - - private static HTTPFileClient createClientOrNull(){ - PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); - if (key == null) { - LOGGER.log(Level.WARNING, "personal key not loaded"); - return null; - } - - return new HTTPFileClient(key.getServerLoginKey(), - key.getBridgeCertificate(), - Config.getInstance().getBoolean(Config.SERV_CERT_VALIDATION)); - } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 73ca7d7a..a90a6886 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -193,6 +193,10 @@ public void onStatusChange(Status status, EnumSet features } } + public void onAuthenticated(JID jid) { + mModel.account().setJID(jid); + } + public void onException(KonException ex) { mViewControl.changed(new ViewEvent.Exception(ex)); } @@ -673,6 +677,10 @@ public void setStatusText(String newStatus) { mClient.sendUserPresence(newStatus); } + public void setAccountPassword(char[] oldPass, char[] newPass) throws KonException { + mModel.account().setPassword(oldPass, newPass); + } + public Path getFilePath(Attachment attachment) { return mAttachmentManager.absoluteFilePath(attachment); } @@ -842,7 +850,7 @@ private void sendNewMessage(Chat chat, String text, Path file) { } private PersonalKey keyOrNull(char[] password) { - Account account = Account.getInstance(); + Account account = mModel.account(); PersonalKey key = account.getPersonalKey().orElse(null); if (key != null) return key; @@ -869,5 +877,10 @@ void changed(ViewEvent event) { this.setChanged(); this.notifyObservers(event); } + + // TODO + public AccountImporter createAccountImporter() { + return new AccountImporter(mModel.account()); + } } } diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index bc88e7ea..eaa7a311 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -55,6 +55,8 @@ import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; import org.kontalk.model.Account; +import org.kontalk.model.Model; +import org.kontalk.system.Control.ViewControl; import org.kontalk.util.Tr; /** @@ -66,11 +68,14 @@ final class ConfigurationDialog extends WebDialog { private final Config mConf = Config.getInstance(); private final View mView; + private final Model mModel; - ConfigurationDialog(JFrame owner, final View view) { + ConfigurationDialog(JFrame owner, View view, Model model) { super(owner); mView = view; + mModel = model; + this.setTitle(Tr.tr("Preferences")); this.setSize(550, 450); this.setResizable(false); @@ -306,13 +311,16 @@ private class AccountPanel extends WebPanel { this.updateKey(); groupPanel.add(new GroupPanel(View.GAP_DEFAULT, fpLabelPanel, mFingerprintArea)); - final WebButton passButton = new WebButton(getPassTitle()); + final WebButton passButton = new WebButton(getPassTitle(mModel.account())); passButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - WebDialog passDialog = createPassDialog(ConfigurationDialog.this); + WebDialog passDialog = createPassDialog( + ConfigurationDialog.this, + mModel.account(), + mView.getControl()); passDialog.setVisible(true); - passButton.setText(getPassTitle()); + passButton.setText(getPassTitle(mModel.account())); } }); groupPanel.add(passButton); @@ -323,7 +331,7 @@ public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) { mView.showImportWizard(false); AccountPanel.this.updateKey(); - passButton.setText(getPassTitle()); + passButton.setText(getPassTitle(mModel.account())); } }); groupPanel.add(importButton); @@ -346,7 +354,7 @@ public void actionPerformed(ActionEvent e) { } private void updateKey() { - PersonalKey key = Account.getInstance().getPersonalKey().orElse(null); + PersonalKey key = mModel.account().getPersonalKey().orElse(null); String uid = key != null ? key.getUserId() : null; mUserIDArea.setText(uid != null ? StringUtils.abbreviate(uid, 30) : @@ -414,20 +422,20 @@ private static WebCheckBox createCheckBox(String title, String tooltip, boolean return checkBox; } - private static String getPassTitle() { - return Account.getInstance().isPasswordProtected() ? + private static String getPassTitle(Account account) { + return account.isPasswordProtected() ? Tr.tr("Change key password") : Tr.tr("Set key password"); } - private static WebDialog createPassDialog(WebDialog parent) { - final WebDialog passDialog = new WebDialog(parent, getPassTitle(), true); + private static WebDialog createPassDialog(WebDialog parent, Account account, ViewControl control) { + final WebDialog passDialog = new WebDialog(parent, getPassTitle(account), true); passDialog.setLayout(new BorderLayout(View.GAP_DEFAULT, View.GAP_DEFAULT)); passDialog.setResizable(false); final WebButton saveButton = new WebButton(Tr.tr("Save")); - boolean passSet = Account.getInstance().isPasswordProtected(); + boolean passSet = account.isPasswordProtected(); final ComponentUtils.PassPanel passPanel = new ComponentUtils.PassPanel(passSet) { @Override void onValidInput() { @@ -450,7 +458,7 @@ public void actionPerformed(ActionEvent e) { return; } try { - Account.getInstance().setPassword(oldPassword, newPassword); + control.setAccountPassword(oldPassword, newPassword); } catch(KonException ex) { LOGGER.log(Level.WARNING, "can't set new password", ex); if (ex.getError() == KonException.Error.CHANGE_PASS_COPY) diff --git a/src/main/java/org/kontalk/view/ImportDialog.java b/src/main/java/org/kontalk/view/ImportDialog.java index e506d350..f4249de1 100644 --- a/src/main/java/org/kontalk/view/ImportDialog.java +++ b/src/main/java/org/kontalk/view/ImportDialog.java @@ -48,7 +48,6 @@ import javax.swing.filechooser.FileNameExtensionFilter; import org.kontalk.misc.KonException; import org.kontalk.system.AccountImporter; -import org.kontalk.model.Account; import org.kontalk.util.Tr; /** @@ -276,7 +275,8 @@ private class ResultPanel extends ImportPanel implements Observer { private boolean mWaiting = false; ResultPanel() { - mImporter = new AccountImporter(this); + mImporter = mView.getControl().createAccountImporter(); + mImporter.addObserver(this); GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); groupPanel.setMargin(View.MARGIN_BIG); @@ -379,7 +379,7 @@ protected void onNext() { char[] newPass = mPassPanel.getNewPassword().orElse(null); if (newPass != null && newPass.length > 0) { try { - Account.getInstance().setPassword(new char[0], newPass); + mView.getControl().setAccountPassword(new char[0], newPass); } catch (KonException ex) { LOGGER.log(Level.WARNING, "can't set password", ex); return; diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 147b8236..54ae5d9b 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -200,7 +200,7 @@ EnumSet serverFeatures() { } void showConfig() { - JDialog configFrame = new ConfigurationDialog(mMainFrame, this); + JDialog configFrame = new ConfigurationDialog(mMainFrame, this, mModel); configFrame.setVisible(true); } From 7ca18dcd531ecf2665eccdf6bd1994b7d82294cc Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 17:06:13 +0100 Subject: [PATCH 198/257] refactoring; less usage of kontalk instance --- src/main/java/org/kontalk/client/Client.java | 10 +++---- .../org/kontalk/system/AttachmentManager.java | 27 +++++++++---------- src/main/java/org/kontalk/system/Control.java | 4 +-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index d25651cb..b277a49e 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -51,7 +52,6 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.pubsub.packet.PubSub; -import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; import org.kontalk.crypto.PersonalKey; @@ -92,7 +92,7 @@ private enum Command {CONNECT, DISCONNECT}; FEATURE_MAP.put(MultipleAddresses.NAMESPACE, ServerFeature.MULTI_ADDRESSING); } - private Client(Control control) { + private Client(Control control, Path appDir) { mControl = control; //mLimited = limited; @@ -104,7 +104,7 @@ private Client(Control control) { mFeatures = EnumSet.noneOf(ServerFeature.class); // setting caps cache - File cacheDir = Kontalk.getInstance().appDir().resolve(CAPS_CACHE_DIR).toFile(); + File cacheDir = appDir.resolve(CAPS_CACHE_DIR).toFile(); if (cacheDir.mkdir()) LOGGER.info("created caps cache directory"); @@ -117,8 +117,8 @@ private Client(Control control) { new SimpleDirectoryPersistentCache(cacheDir)); } - public static Client create(Control control) { - Client client = new Client(control); + public static Client create(Control control, Path appDir) { + Client client = new Client(control, appDir); Thread clientThread = new Thread(client, "Client Connector"); clientThread.setDaemon(true); diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index e8f49948..67d8363f 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -33,7 +33,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; -import org.kontalk.Kontalk; import org.kontalk.client.HTTPFileClient; import org.kontalk.crypto.Coder; import org.kontalk.crypto.Coder.Encryption; @@ -81,9 +80,9 @@ public class AttachmentManager implements Runnable { // TODO get this from server private static final URI UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); - private final LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); - private final Control mControl; + + private final LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); private final Path mAttachmentDir; private final Path mPreviewDir; @@ -108,7 +107,7 @@ public DownloadTask(InMessage message) { } } - private AttachmentManager(Path baseDir, Control control) { + private AttachmentManager(Control control, Path baseDir) { mControl = control; mAttachmentDir = baseDir.resolve(ATT_DIRNAME); if (mAttachmentDir.toFile().mkdir()) @@ -119,6 +118,16 @@ private AttachmentManager(Path baseDir, Control control) { LOGGER.info("created preview directory"); } + static AttachmentManager create(Control control, Path appDir) { + AttachmentManager manager = new AttachmentManager(control, appDir); + + Thread thread = new Thread(manager, "Attachment Transfer"); + thread.setDaemon(true); + thread.start(); + + return manager; + } + void queueUpload(OutMessage message) { boolean added = mQueue.offer(new Task.UploadTask(message)); if (!added) { @@ -383,16 +392,6 @@ public static boolean isImage(String mimeType) { return mimeType.startsWith("image"); } - static AttachmentManager create(Control control) { - AttachmentManager manager = new AttachmentManager(Kontalk.getInstance().appDir(), control); - - Thread thread = new Thread(manager, "Attachment Transfer"); - thread.setDaemon(true); - thread.start(); - - return manager; - } - /** * Create a new attachment for a given file denoted by its path. */ diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index a90a6886..ed1c9d9a 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -108,9 +108,9 @@ private Control(Path appDir) throws KonException { mModel = new Model(mDB, appDir); - mClient = Client.create(this); + mClient = Client.create(this, appDir); mChatStateManager = new ChatStateManager(mClient); - mAttachmentManager = AttachmentManager.create(this); + mAttachmentManager = AttachmentManager.create(this, appDir); mRosterHandler = new RosterHandler(this, mClient, mModel); mAvatarHandler = new AvatarHandler(mClient, mModel); mGroupControl = new GroupControl(this, mModel); From 63d9cd6c2e662a7629e702ddadfb4c3feeadd14b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 19:20:01 +0100 Subject: [PATCH 199/257] model: refactoring; user avatar not singleton anymore --- src/main/java/org/kontalk/model/Account.java | 3 +- src/main/java/org/kontalk/model/Avatar.java | 42 +++++++------------ src/main/java/org/kontalk/model/Contact.java | 6 +++ src/main/java/org/kontalk/model/Model.java | 25 ++++++++++- .../org/kontalk/system/AvatarHandler.java | 5 ++- src/main/java/org/kontalk/system/Control.java | 8 ++-- src/main/java/org/kontalk/view/MainFrame.java | 2 +- .../java/org/kontalk/view/ProfileDialog.java | 6 +-- 8 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index 0b2ef3de..e2d22d39 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -53,8 +53,6 @@ public final class Account { private static final String PRIVATE_KEY_FILENAME = "kontalk-private.asc"; private static final String BRIDGE_CERT_FILENAME = "kontalk-login.crt"; - private static Account INSTANCE = null; - private final Path mKeyDir; private final Config mConf; @@ -198,6 +196,7 @@ private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } + // TODO public static JID getUserJID() { return JID.bare(Config.getInstance().getString(Config.ACC_JID)); } diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index d418330e..7fdc251f 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -22,6 +22,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.Objects; import java.util.Optional; import java.util.logging.Level; @@ -123,18 +124,21 @@ public static class UserAvatar extends Avatar { private static final int MAX_SIZE = 150; private static final String USER_FILENAME = "avatar"; - private static UserAvatar USER_AVATAR = null; - private byte[] mImageData = null; /** Saved user Avatar. */ - UserAvatar() { - super(userFile()); + UserAvatar(Path appDir) { + super(userFile(appDir)); } /** New user Avatar. ID generated from image. */ - UserAvatar(BufferedImage image) { - super(id(image), userFile(), image); + private UserAvatar(BufferedImage image, Path appDir) { + super(id(image), userFile(appDir), image); + } + + static UserAvatar create(BufferedImage image, Path appDir) { + image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE); + return new UserAvatar(image, appDir); } @Override @@ -151,31 +155,13 @@ public Optional imageData() { return Optional.ofNullable(mImageData); } - private static File userFile() { - return Kontalk.getInstance().appDir().resolve(USER_FILENAME + "." + FORMAT).toFile(); - } - - public static UserAvatar instance() { - if (USER_AVATAR == null) { - USER_AVATAR = new UserAvatar(); - } - return USER_AVATAR; - } - - public static UserAvatar setImage(BufferedImage image) { - image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE); - USER_AVATAR = new UserAvatar(image); - return USER_AVATAR; - } - - public static void deleteImage() { - USER_AVATAR.delete(); - USER_AVATAR = new UserAvatar(); + private static File userFile(Path appDir) { + return appDir.resolve(USER_FILENAME + "." + FORMAT).toFile(); } } - public static void createDir() { - boolean created = Kontalk.getInstance().appDir().resolve(DIR).toFile().mkdir(); + public static void createDir(Path appDir) { + boolean created = appDir.resolve(DIR).toFile().mkdir(); if (created) LOGGER.info("created avatar directory"); } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index ae85a9cd..c19e0976 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -38,6 +38,12 @@ /** * A contact in the Kontalk/XMPP-Jabber network. * + * TODO group chats need some weaker entity here: not deletable, + * not shown in ui contact list(?), but with public key + * + * idea: "deletable" or / "weak" field: contact gets deleted + * when no group chat exists anymore + * * @author Alexander Bikadorov {@literal } */ public final class Contact extends Observable { diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index fbc7708b..2651c709 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -18,9 +18,11 @@ package org.kontalk.model; +import java.awt.image.BufferedImage; import java.nio.file.Path; import java.util.Map; import java.util.logging.Logger; +import org.kontalk.model.Avatar.UserAvatar; import org.kontalk.model.chat.ChatList; import org.kontalk.system.Config; import org.kontalk.system.Database; @@ -32,14 +34,22 @@ public final class Model { private static final Logger LOGGER = Logger.getLogger(Model.class.getName()); + private final Path mAppDir; + private final ContactList mContactList; private final ChatList mChatList; private final Account mAccount; + private UserAvatar mUserAvatar; + public Model(Database db, Path appDir) { - mAccount = new Account(appDir, Config.getInstance()); + mAppDir = appDir; + + mAccount = new Account(mAppDir, Config.getInstance()); mContactList = new ContactList(db); mChatList = new ChatList(db); + + mUserAvatar = new UserAvatar(mAppDir); } public Account account() { @@ -54,9 +64,22 @@ public ChatList chats() { return mChatList; } + public UserAvatar userAvatar() { + return mUserAvatar; + } + public void load() { // order matters! Map contactMap = mContactList.load(); mChatList.load(contactMap); } + + public UserAvatar newUserAvatar(BufferedImage image) { + return UserAvatar.create(image, mAppDir); + } + + public void deleteAvatar() { + mUserAvatar.delete(); + mUserAvatar = new UserAvatar(mAppDir); + } } diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 54cf9314..f2faa887 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -19,6 +19,7 @@ package org.kontalk.system; import java.awt.image.BufferedImage; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; @@ -45,11 +46,11 @@ public final class AvatarHandler { private final Client mClient; private final Model mModel; - AvatarHandler(Client client, Model model) { + AvatarHandler(Client client, Model model, Path appDir) { mClient = client; mModel = model; - Avatar.createDir(); + Avatar.createDir(appDir); } public void onNotify(JID jid, String id) { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index ed1c9d9a..b1c7be16 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -112,7 +112,7 @@ private Control(Path appDir) throws KonException { mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(this, appDir); mRosterHandler = new RosterHandler(this, mClient, mModel); - mAvatarHandler = new AvatarHandler(mClient, mModel); + mAvatarHandler = new AvatarHandler(mClient, mModel, appDir); mGroupControl = new GroupControl(this, mModel); } @@ -815,7 +815,7 @@ public void sendAttachment(Chat chat, Path file){ } public void setUserAvatar(BufferedImage image) { - Avatar.UserAvatar newAvatar = Avatar.UserAvatar.setImage(image); + Avatar.UserAvatar newAvatar = mModel.newUserAvatar(image); byte[] avatarData = newAvatar.imageData().orElse(null); if (avatarData == null || newAvatar.getID().isEmpty()) return; @@ -824,12 +824,12 @@ public void setUserAvatar(BufferedImage image) { } public void unsetUserAvatar(){ - if (Avatar.UserAvatar.instance().getID().isEmpty()) + if (mModel.userAvatar().getID().isEmpty()) return; boolean succ = mClient.deleteAvatar(); if (succ) - Avatar.UserAvatar.deleteImage(); + mModel.deleteAvatar(); } /* private */ diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 8226e00e..b10a3ff7 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -142,7 +142,7 @@ public void actionPerformed(ActionEvent event) { statusMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - WebDialog statusDialog = new ProfileDialog(mView); + WebDialog statusDialog = new ProfileDialog(mView, model); statusDialog.setVisible(true); } }); diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index f0959e30..5e9b4ea1 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -48,7 +48,7 @@ import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.ObjectUtils; import org.kontalk.client.Client; -import org.kontalk.model.Avatar; +import org.kontalk.model.Model; import org.kontalk.system.Config; import org.kontalk.system.Control; import org.kontalk.util.MediaUtils; @@ -71,7 +71,7 @@ final class ProfileDialog extends WebDialog { private BufferedImage mNewImage = null; - ProfileDialog(View view) { + ProfileDialog(View view, Model model) { mView = view; this.setTitle(Tr.tr("User Profile")); @@ -91,7 +91,7 @@ final class ProfileDialog extends WebDialog { groupPanel.add(new WebLabel(Tr.tr("Your profile picture:"))); mAvatarImage = new WebImage(); - mOldImage = mNewImage = Avatar.UserAvatar.instance().loadImage().orElse(null); + mOldImage = mNewImage = model.userAvatar().loadImage().orElse(null); this.setImage(mOldImage); //mAvatarImage.setDisplayType(DisplayType.fitComponent); From 6b3b0d764a6ecc73a341f2bc936dc5b2b784eb70 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 19:28:09 +0100 Subject: [PATCH 200/257] rework of application initialization --- src/main/java/org/kontalk/Kontalk.java | 15 +++- src/main/java/org/kontalk/system/Control.java | 77 ++++++++----------- src/main/java/org/kontalk/view/View.java | 2 +- src/test/java/org/kontalk/KontalkTest.java | 12 --- 4 files changed, 45 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 5d34bbcd..2441c7d2 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -148,12 +148,23 @@ int start(boolean ui) { Control control; try { - control = Control.create(mAppDir); + control = new Control(mAppDir); } catch (KonException ex) { LOGGER.log(Level.SEVERE, "can't create application", ex); return 5; } + // handle shutdown signals/System.exit() calls + Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { + @Override + public void run() { + // NOTE: logging does not work here anymore + control.shutDown(false); + Kontalk.this.removeLock(); + System.out.println("Kontalk: shutdown finished"); + } + }); + control.launch(ui); return 0; @@ -163,7 +174,7 @@ public Path appDir() { return mAppDir; } - public boolean removeLock() { + private boolean removeLock() { if (mRunLock == null) { LOGGER.warning("no lock"); return false; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index b1c7be16..af686e1f 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -35,7 +35,6 @@ import java.util.stream.Collectors; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; -import org.kontalk.Kontalk; import org.kontalk.client.Client; import org.kontalk.client.KonMessageSender; import org.kontalk.crypto.Coder; @@ -96,7 +95,7 @@ public enum Status { private boolean mShuttingDown = false; - private Control(Path appDir) throws KonException { + public Control(Path appDir) throws KonException { mViewControl = new ViewControl(); try { @@ -116,22 +115,6 @@ private Control(Path appDir) throws KonException { mGroupControl = new GroupControl(this, mModel); } - public static Control create(Path appDir) throws KonException { - Control control = new Control(appDir); - - // handle shutdown signals - Runtime.getRuntime().addShutdownHook(new Thread("Shutdown Hook") { - @Override - public void run() { - // NOTE: logging does not work here anymore - control.mViewControl.shutDown(false); - System.out.println("Kontalk: shutdown finished"); - } - }); - - return control; - } - public void launch(boolean ui) { mModel.load(); @@ -139,7 +122,7 @@ public void launch(boolean ui) { if (ui) { View view = View.create(mViewControl, mModel).orElse(null); if (view == null) { - mViewControl.shutDown(false); + this.shutDown(false); return; } view.init(); @@ -156,6 +139,32 @@ public void launch(boolean ui) { mViewControl.connect(); } + public void shutDown(boolean exit) { + if (mShuttingDown) + // we were already here + return; + + mShuttingDown = true; + + LOGGER.info("Shutting down..."); + mViewControl.disconnect(); + + mViewControl.changed(new ViewEvent.StatusChange(Status.SHUTTING_DOWN, + EnumSet.noneOf(Client.ServerFeature.class))); + try { + mDB.close(); + } catch (RuntimeException ex) { + LOGGER.log(Level.WARNING, "can't close database", ex); + } + + Config.getInstance().saveToFile(); + + if (exit) { + LOGGER.info("exit"); + System.exit(0); + } + } + public RosterHandler getRosterHandler() { return mRosterHandler; } @@ -415,7 +424,7 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { } return this.sendMessage(newMessage); - } + } boolean sendMessage(OutMessage message) { MessageContent content = message.getContent(); @@ -617,32 +626,8 @@ private Optional findMessage(MessageIDs ids) { public class ViewControl extends Observable { - public void shutDown(boolean exit) { - if (mShuttingDown) - // we were already here - return; - - mShuttingDown = true; - - LOGGER.info("Shutting down..."); - this.disconnect(); - - this.changed(new ViewEvent.StatusChange(Status.SHUTTING_DOWN, - EnumSet.noneOf(Client.ServerFeature.class))); - try { - mDB.close(); - } catch (RuntimeException ex) { - LOGGER.log(Level.WARNING, "can't close database", ex); - } - - Config.getInstance().saveToFile(); - - Kontalk.getInstance().removeLock(); - - if (exit) { - LOGGER.info("exit"); - System.exit(0); - } + public void shutDown() { + Control.this.shutDown(true); } public void connect() { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 54ae5d9b..ce8614bf 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -341,7 +341,7 @@ ViewControl getControl() { void callShutDown() { // trigger save if contact details are shown mContent.showNothing(); - mControl.shutDown(true); + mControl.shutDown(); } /* view internal */ diff --git a/src/test/java/org/kontalk/KontalkTest.java b/src/test/java/org/kontalk/KontalkTest.java index 75026a0d..42651a64 100644 --- a/src/test/java/org/kontalk/KontalkTest.java +++ b/src/test/java/org/kontalk/KontalkTest.java @@ -28,7 +28,6 @@ import org.junit.rules.TemporaryFolder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import org.junit.Ignore; @@ -84,17 +83,6 @@ public void testAppDir() { assertEquals(result, APP_DIR); } - /** - * Test of removeLock method, of class Kontalk. - */ - @Test - public void testRemoveLock() { - System.out.println("removeLock"); - Kontalk.getInstance().start(false); - boolean succ = Kontalk.getInstance().removeLock(); - assertTrue(succ); - } - /** * Test of getInstance method, of class Kontalk. */ From 5afdef9c5ae8522547b714dafe1d34acc8d3865c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 17 Mar 2016 19:34:56 +0100 Subject: [PATCH 201/257] system: refactored; initialize configuration --- src/main/java/org/kontalk/system/Config.java | 21 +++++++++++++------ src/main/java/org/kontalk/system/Control.java | 2 ++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/system/Config.java index 8583a39f..4b4189d5 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/system/Config.java @@ -26,7 +26,6 @@ import java.util.logging.Logger; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; -import org.kontalk.Kontalk; import org.kontalk.util.Tr; /** @@ -70,7 +69,8 @@ public final class Config extends PropertiesConfiguration { //public static final String DEFAULT_SERV_NET = "kontalk.net"; public static final String DEFAULT_SERV_HOST = "beta.kontalk.net"; public static final int DEFAULT_SERV_PORT = 5999; - private static final String DEFAULT_XMPP_STATUS = + + private final String mDefaultXMPPStatus = Tr.tr("Hey, I'm using Kontalk on my PC!"); private Config(Path configFile) { @@ -102,7 +102,7 @@ private Config(Path configFile) { map.put(VIEW_USER_CONTACT, false); map.put(NET_SEND_CHAT_STATE, true); map.put(NET_SEND_ROSTER_NAME, false); - map.put(NET_STATUS_LIST, new String[]{DEFAULT_XMPP_STATUS}); + map.put(NET_STATUS_LIST, new String[]{mDefaultXMPPStatus}); map.put(NET_AUTO_SUBSCRIPTION, false); map.put(NET_REQUEST_AVATARS, true); map.put(NET_MAX_IMG_SIZE, -1); @@ -126,10 +126,19 @@ public void saveToFile() { } } - public synchronized static Config getInstance() { - if (INSTANCE == null) { - INSTANCE = new Config(Kontalk.getInstance().appDir().resolve(Config.FILENAME)); + static void initialize(Path appDir) { + if (INSTANCE != null) { + LOGGER.warning("already initialized"); + return; } + + INSTANCE = new Config(appDir.resolve(Config.FILENAME)); + } + + public synchronized static Config getInstance() { + if (INSTANCE == null) + throw new IllegalStateException("not initialized"); + return INSTANCE; } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index af686e1f..5acf3337 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -98,6 +98,8 @@ public enum Status { public Control(Path appDir) throws KonException { mViewControl = new ViewControl(); + Config.initialize(appDir); + try { mDB = new Database(appDir); } catch (KonException ex) { From ef447fcc148bec3fb092c4359a8665efa5e02fff Mon Sep 17 00:00:00 2001 From: Josh Cheung Date: Tue, 22 Mar 2016 02:08:40 +0000 Subject: [PATCH 202/257] Translated using Weblate (Chinese) Currently translated at 100.0% (259 of 259 strings) --- src/main/resources/i18n/strings_zh.properties | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/resources/i18n/strings_zh.properties b/src/main/resources/i18n/strings_zh.properties index a8420cbd..bc9ac2c9 100644 --- a/src/main/resources/i18n/strings_zh.properties +++ b/src/main/resources/i18n/strings_zh.properties @@ -228,3 +228,37 @@ s_KRQ6=输入密码…… s_2R2P=正在载入…… s_307W=正在下载…… s_4WC0=服务器证书无法被验证。 +s_68LN=请求 +s_7MDX=从联系人请求状态验证 +s_OTEU=自动授权验证 +s_MFZY=自动从其他用户授权在线状态验证 +s_FAS6=发送聊天活动(如正在输入…)给其他用户 +s_29CW=编辑联系人 +s_AGYA=编辑联系人设置 +s_MNIJ=正在等待… +s_0WUC=未验证 +s_9S4L=验证申请 +s_FDTC=若确认,该联系人将能够看到你的在线状态。 +s_TAG1=用户资料 +s_84VP=编辑个人资料 +s_4QXX=你的头像: +s_1R7I=资料 +s_O2UH=设置用户资料 +s_6SC9=用户ID: +s_6K2I=下载资料中的头像 +s_08GL=下载联系人资料的头像 +s_T2TO=联系人中显示本人 +s_F1X0=联系人列表中显示本人 +s_3PFR=移除 +s_D7FH=不受服务器支持 +s_WUDV=组管理 +s_RCDY=网络 +s_LPE1=网络设置 +s_R4F8=原始 +s_K83J=小(300万像素) +s_RAMV=中(500万像素) +s_7CPG=大(800万像素) +s_5RTB=发送前减小图片尺寸 +s_13FX=调整图片附件尺寸: +s_8OBK=发送文件 +s_7YWF=最大文件: From a45f556281d1b7e529d32ab7d77fb6667b5aea03 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 27 Mar 2016 15:34:39 +0200 Subject: [PATCH 203/257] model: refactored, static app dir in model class --- src/main/java/org/kontalk/model/Avatar.java | 4 +--- src/main/java/org/kontalk/model/Model.java | 12 ++++++++---- src/main/java/org/kontalk/system/Control.java | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index 7fdc251f..b850f404 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -29,7 +29,6 @@ import java.util.logging.Logger; import javax.imageio.ImageIO; import org.apache.commons.codec.digest.DigestUtils; -import org.kontalk.Kontalk; import org.kontalk.util.MediaUtils; @@ -64,7 +63,7 @@ private Avatar(String id, File file, BufferedImage image) { mID = id; mFile = file != null ? file : - Kontalk.getInstance().appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); + Model.appDir().resolve(DIR).resolve(id + "." + FORMAT).toFile(); mImage = image; if (mImage != null) { @@ -184,7 +183,6 @@ private static byte[] imageData(BufferedImage image) { return null; } return out.toByteArray(); - } } diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index 2651c709..95dc9c0c 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -21,7 +21,6 @@ import java.awt.image.BufferedImage; import java.nio.file.Path; import java.util.Map; -import java.util.logging.Logger; import org.kontalk.model.Avatar.UserAvatar; import org.kontalk.model.chat.ChatList; import org.kontalk.system.Config; @@ -32,8 +31,8 @@ * @author Alexander Bikadorov {@literal } */ public final class Model { - private static final Logger LOGGER = Logger.getLogger(Model.class.getName()); + private static Path APP_DIR; private final Path mAppDir; private final ContactList mContactList; @@ -43,7 +42,7 @@ public final class Model { private UserAvatar mUserAvatar; public Model(Database db, Path appDir) { - mAppDir = appDir; + mAppDir = APP_DIR = appDir; mAccount = new Account(mAppDir, Config.getInstance()); mContactList = new ContactList(db); @@ -78,8 +77,13 @@ public UserAvatar newUserAvatar(BufferedImage image) { return UserAvatar.create(image, mAppDir); } - public void deleteAvatar() { + public void deleteUserAvatar() { mUserAvatar.delete(); mUserAvatar = new UserAvatar(mAppDir); } + + // TODO + static Path appDir() { + return APP_DIR; + } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 5acf3337..61c59f0d 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -816,7 +816,7 @@ public void unsetUserAvatar(){ boolean succ = mClient.deleteAvatar(); if (succ) - mModel.deleteAvatar(); + mModel.deleteUserAvatar(); } /* private */ From 5e4cc177f9f627e477b3b0bcbc6b3f2e4b758546 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 27 Mar 2016 15:55:04 +0200 Subject: [PATCH 204/257] refactoring; main class not singleton anymore --- src/main/java/org/kontalk/Kontalk.java | 38 +++++----------------- src/test/java/org/kontalk/KontalkTest.java | 25 ++------------ 2 files changed, 11 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 2441c7d2..7fe36d08 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -54,32 +54,16 @@ public final class Kontalk { public static final String VERSION = "3.0.4"; - private static Kontalk INSTANCE = null; - + private final Path mAppDir; private ServerSocket mRunLock = null; - private Path mAppDir = null; - private static void ensureInitialized() { + Kontalk() { // platform dependent configuration directory - ensureInitialized(Paths.get(System.getProperty("user.home"), + this(Paths.get(System.getProperty("user.home"), SystemUtils.IS_OS_WINDOWS ? "Kontalk" : ".kontalk")); } - static void ensureInitialized(Path appDir) { - if (INSTANCE != null) - return; - - INSTANCE = new Kontalk(appDir); - } - - public static Kontalk getInstance() { - if (INSTANCE == null) - throw new IllegalStateException("application not initialized"); - - return INSTANCE; - } - - private Kontalk(Path appDir) { + Kontalk(Path appDir) { mAppDir = appDir.toAbsolutePath(); } @@ -170,10 +154,6 @@ public void run() { return 0; } - public Path appDir() { - return mAppDir; - } - private boolean removeLock() { if (mRunLock == null) { LOGGER.warning("no lock"); @@ -221,13 +201,13 @@ public static void main(String[] args) { String appDir = cmd.getOptionValue("d", ""); - if (!appDir.isEmpty()) - Kontalk.ensureInitialized(Paths.get(appDir)); - else - Kontalk.ensureInitialized(); + Kontalk app = !appDir.isEmpty() ? + new Kontalk(Paths.get(appDir)) : + new Kontalk(); - int returnCode = Kontalk.getInstance().start(!cmd.hasOption("c")); + int returnCode = app.start(!cmd.hasOption("c")); if (returnCode != 0) + // didn't work System.exit(returnCode); new Thread("Kontalk Main") { diff --git a/src/test/java/org/kontalk/KontalkTest.java b/src/test/java/org/kontalk/KontalkTest.java index 42651a64..89411909 100644 --- a/src/test/java/org/kontalk/KontalkTest.java +++ b/src/test/java/org/kontalk/KontalkTest.java @@ -27,7 +27,6 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import org.junit.Ignore; @@ -47,7 +46,6 @@ public KontalkTest() { @BeforeClass public static void setUpClass() { APP_DIR = TEMP_FOLDER.getRoot().toPath().resolve("app_dir"); - Kontalk.ensureInitialized(APP_DIR); } @AfterClass @@ -68,31 +66,12 @@ public void tearDown() { @Test public void testStart() { System.out.println("start"); - int returnCode = Kontalk.getInstance().start(false); + Kontalk app = new Kontalk(APP_DIR); + int returnCode = app.start(false); assertEquals(returnCode, 0); // TODO stop } - /** - * Test of appDir method, of class Kontalk. - */ - @Test - public void testAppDir() { - System.out.println("appDir"); - Path result = Kontalk.getInstance().appDir(); - assertEquals(result, APP_DIR); - } - - /** - * Test of getInstance method, of class Kontalk. - */ - @Test - public void testGetInstance() { - System.out.println("getInstance"); - Kontalk result = Kontalk.getInstance(); - assertNotNull(result); - } - /** * Test of main method, of class Kontalk. */ From 0c53debb21ad403f7f365572b50079ad8f364399 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 27 Mar 2016 17:56:34 +0200 Subject: [PATCH 205/257] crypto: minor coding rework --- src/main/java/org/kontalk/crypto/Coder.java | 2 +- .../java/org/kontalk/crypto/Decryptor.java | 19 +++++------- .../java/org/kontalk/crypto/Encryptor.java | 29 +++++++------------ 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index c0a12dd5..77554d57 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -59,7 +59,7 @@ public static enum Signing {NOT, SIGNED, VERIFIED, UNKNOWN} public static enum Error { /** Some unknown error. */ UNKNOWN_ERROR, - /** Own personal key not found. */ + /** Own personal key not found. Unused. */ MY_KEY_UNAVAILABLE, /** Public key of sender not found. */ KEY_UNAVAILABLE, diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 585dbdf0..c0118bc3 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -75,7 +75,6 @@ private static class DecryptionResult { private final DecryptMessage mMessage; private final PersonalKey mMyKey; - // nullable private final PGPUtils.PGPCoderKey mSenderKey; @@ -126,7 +125,8 @@ boolean decryptMessage() { String decrText = EncodingUtils.getString( plainOut.toByteArray(), CPIMMessage.CHARSET); - MessageContent content = this.parseCPIMOrNull(decrText, myUID, Optional.ofNullable(senderUID)); + MessageContent content = parseCPIMOrNull(mMessage, decrText, myUID, + Optional.ofNullable(senderUID)); // set errors mMessage.setSecurityErrors(allErrors); @@ -152,12 +152,7 @@ void decryptAttachment(Path baseDir) { MessageContent.Attachment attachment = inMessage.getContent().getAttachment().orElse(null); if (attachment == null) { - LOGGER.warning("no attachment in in-message"); - return; - } - - if (mMyKey == null) { - mMessage.setSecurityErrors(EnumSet.of(Coder.Error.MY_KEY_UNAVAILABLE)); + LOGGER.warning("no attachment in message"); return; } @@ -343,7 +338,7 @@ private static DecryptionResult verifySignature(DecryptionResult result, * * The decrypted content of a message is in CPIM format. */ - private MessageContent parseCPIMOrNull(String cpim, + private static MessageContent parseCPIMOrNull(DecryptMessage message, String cpim, String myUid, Optional senderKeyUID) { CPIMMessage cpimMessage; @@ -351,7 +346,7 @@ private MessageContent parseCPIMOrNull(String cpim, cpimMessage = CPIMMessage.parse(cpim); } catch (ParseException ex) { LOGGER.log(Level.WARNING, "can't find valid CPIM data", ex); - mMessage.setSecurityErrors(EnumSet.of(Coder.Error.INVALID_DATA)); + message.setSecurityErrors(EnumSet.of(Coder.Error.INVALID_DATA)); return null; } @@ -390,7 +385,7 @@ private MessageContent parseCPIMOrNull(String cpim, } catch (XmlPullParserException | IOException | SmackException ex) { LOGGER.log(Level.WARNING, "can't parse XMPP XML string", ex); errors.add(Coder.Error.INVALID_DATA); - mMessage.setSecurityErrors(errors); + message.setSecurityErrors(errors); return null; } LOGGER.config("decrypted XML: "+m.toXML()); @@ -400,7 +395,7 @@ private MessageContent parseCPIMOrNull(String cpim, decryptedContent = MessageContent.plainText(content); } - mMessage.setSecurityErrors(errors); + message.setSecurityErrors(errors); return decryptedContent; } } diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index d4f4bf75..4fbdf1cb 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -67,8 +67,6 @@ final class Encryptor { private final PersonalKey myKey; private final OutMessage message; - private List receiverKeys = null; - Encryptor(PersonalKey myKey, OutMessage message) { this.myKey = myKey; this.message = message; @@ -89,8 +87,8 @@ private Optional encryptData(String data, String mime) { return Optional.empty(); } - boolean loaded = this.loadKeys(); - if (!loaded) + List receiverKeys = this.loadKeysOrNull(); + if (receiverKeys == null) return Optional.empty(); // secure the message against replay attacks using Message/CPIM @@ -123,8 +121,8 @@ private Optional encryptData(String data, String mime) { } Optional encryptAttachment(File file) { - boolean loaded = this.loadKeys(); - if (!loaded) + List receiverKeys = this.loadKeysOrNull(); + if (receiverKeys == null) return Optional.empty(); File tempFile; @@ -147,23 +145,18 @@ Optional encryptAttachment(File file) { return Optional.of(tempFile); } - private boolean loadKeys() { + private List loadKeysOrNull() { List contacts = message.getTransmissions().stream() .map(t -> t.getContact()) .collect(Collectors.toList()); - receiverKeys = receiverKeysOrNull(contacts); - if (receiverKeys == null) { - message.setSecurityErrors(EnumSet.of(Coder.Error.KEY_UNAVAILABLE)); - return false; - } - return true; - } - - private static List receiverKeysOrNull(List contacts) { - List keys = contacts.stream() + List receiverKeys = contacts.stream() .map(c -> Coder.contactkey(c).orElse(null)) .collect(Collectors.toList()); - return keys.stream().anyMatch(Objects::isNull) ? null : keys; + if (receiverKeys.stream().anyMatch(Objects::isNull)) { + message.setSecurityErrors(EnumSet.of(Coder.Error.KEY_UNAVAILABLE)); + return null; + } + return receiverKeys; } /** From bd9b56068898245073b2e48764342f275c507d6c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 28 Mar 2016 16:02:44 +0200 Subject: [PATCH 206/257] make multiple running applications with different app directory possible --- src/main/java/org/kontalk/Kontalk.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 7fe36d08..362a90c2 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -69,9 +69,10 @@ public final class Kontalk { int start(boolean ui) { // check if already running + int port = (1 << 14) + (1 << 15) + mAppDir.hashCode() % (1 << 14); try { InetAddress addr = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); - mRunLock = new ServerSocket(9871, 10, addr); + mRunLock = new ServerSocket(port, 10, addr); } catch(java.net.BindException ex) { LOGGER.severe("already running"); return 2; @@ -127,7 +128,7 @@ int start(boolean ui) { // fix crypto restriction CryptoUtils.removeCryptographyRestrictions(); - // register provider + // register security provider PGPUtils.registerProvider(); Control control; From fe5d5d8ded568531c600e3f85276d8ea8cbdb1b3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 29 Mar 2016 13:44:52 +0200 Subject: [PATCH 207/257] control: fix obsolete references to deleted chat objects --- src/main/java/org/kontalk/system/ChatStateManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index b3b8f455..86851448 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -18,10 +18,10 @@ package org.kontalk.system; -import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; +import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.client.Client; @@ -38,7 +38,7 @@ final class ChatStateManager { private static final int COMPOSING_TO_PAUSED = 15; // seconds private final Client mClient; - private final Map mChatStateCache = new HashMap<>(); + private final Map mChatStateCache = new WeakHashMap<>(); private final Timer mTimer = new Timer("Chat State Timer", true); public ChatStateManager(Client client) { From 3b429be44619d5c63618338869ee1d726d06cf86 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 30 Mar 2016 20:20:50 +0200 Subject: [PATCH 208/257] model: model class singleton, can't get rid of these --- src/main/java/org/kontalk/model/Avatar.java | 8 +- src/main/java/org/kontalk/model/Contact.java | 13 ++- .../java/org/kontalk/model/ContactList.java | 14 ++- src/main/java/org/kontalk/model/Model.java | 88 ++++++++++++++++--- .../java/org/kontalk/model/chat/Chat.java | 30 +++---- .../java/org/kontalk/model/chat/ChatList.java | 15 ++-- .../org/kontalk/model/chat/GroupChat.java | 37 ++++---- .../org/kontalk/model/chat/SingleChat.java | 9 +- .../org/kontalk/model/message/InMessage.java | 11 ++- .../org/kontalk/model/message/KonMessage.java | 24 +++-- .../org/kontalk/model/message/OutMessage.java | 12 +-- .../kontalk/model/message/Transmission.java | 15 ++-- .../org/kontalk/system/AvatarHandler.java | 5 +- src/main/java/org/kontalk/system/Control.java | 36 +++----- 14 files changed, 176 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index b850f404..e4dec2ba 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -135,9 +135,9 @@ private UserAvatar(BufferedImage image, Path appDir) { super(id(image), userFile(appDir), image); } - static UserAvatar create(BufferedImage image, Path appDir) { + static UserAvatar create(BufferedImage image) { image = MediaUtils.scale(image, MAX_SIZE, MAX_SIZE); - return new UserAvatar(image, appDir); + return new UserAvatar(image, Model.appDir()); } @Override @@ -159,13 +159,13 @@ private static File userFile(Path appDir) { } } - public static void createDir(Path appDir) { + static void createStorageDir(Path appDir) { boolean created = appDir.resolve(DIR).toFile().mkdir(); if (created) LOGGER.info("created avatar directory"); } - public static Avatar deleted() { + static Avatar deleted() { return new Avatar(""); } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index c19e0976..dfbbbfcf 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -84,7 +84,6 @@ public static enum Subscription { COL_AVATAR_ID + " TEXT" + ")"; - private final Database mDB; private final int mID; private JID mJID; private String mName; @@ -100,8 +99,7 @@ public static enum Subscription { private Avatar mAvatar = null; // new contact (eg from roster) - Contact(Database db, JID jid, String name) { - mDB = db; + Contact(JID jid, String name) { mJID = jid; mName = name; @@ -115,13 +113,13 @@ public static enum Subscription { null, // key null, // fingerprint null); // avatar id - mID = mDB.execInsert(TABLE, values); + mID = Model.database().execInsert(TABLE, values); if (mID < 1) LOGGER.log(Level.WARNING, "could not insert contact"); } // loading from database - public Contact(Database db, + public Contact( int id, JID jid, String name, @@ -131,7 +129,6 @@ public Contact(Database db, String publicKey, String fingerprint, String avatarID) { - mDB = db; mID = id; mJID = jid; mName = name; @@ -342,7 +339,7 @@ private void save() { set.put(COL_PUB_KEY, Database.setString(mKey)); set.put(COL_KEY_FP, Database.setString(mFingerprint)); set.put(COL_AVATAR_ID, Database.setString(mAvatar != null ? mAvatar.getID() : "")); - mDB.execUpdate(TABLE, set, mID); + Model.database().execUpdate(TABLE, set, mID); } private void changed(Object arg) { @@ -387,7 +384,7 @@ static Contact load(Database db, ResultSet rs) throws SQLException { String fp = Database.getString(rs, Contact.COL_KEY_FP); String avatarID = Database.getString(rs, Contact.COL_AVATAR_ID); - return new Contact(db, id, jid, name, status, + return new Contact(id, jid, name, status, Optional.ofNullable(lastSeen), encr, key, fp, avatarID); } } diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index f825943a..e9c80bf2 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -35,7 +35,7 @@ /** * Global list of all contacts. * - * Does not contain deleted user (only accessible by database ID). + * Does not contain deleted user. * * @author Alexander Bikadorov {@literal } */ @@ -43,23 +43,21 @@ public final class ContactList extends Observable implements Iterable { private static final Logger LOGGER = Logger.getLogger(ContactList.class.getName()); - private final Database mDB; /** JID to contact. Without deleted contacts. */ private final Map mJIDMap = Collections.synchronizedMap(new HashMap()); - ContactList(Database db) { - mDB = db; - } + ContactList() {} Map load() { assert mJIDMap.isEmpty(); Map contactMap = new HashMap<>(); - try (ResultSet resultSet = mDB.execSelectAll(Contact.TABLE)) { + Database db = Model.database(); + try (ResultSet resultSet = db.execSelectAll(Contact.TABLE)) { while (resultSet.next()) { - Contact contact = Contact.load(mDB, resultSet); + Contact contact = Contact.load(db, resultSet); JID jid = contact.getJID(); if (mJIDMap.containsKey(jid)) { @@ -86,7 +84,7 @@ public Optional create(JID jid, String name) { if (!this.isValid(jid)) return Optional.empty(); - Contact newContact = new Contact(mDB, jid, name); + Contact newContact = new Contact(jid, name); if (newContact.getID() < 1) return Optional.empty(); diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index 95dc9c0c..6808d7b4 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -20,20 +20,32 @@ import java.awt.image.BufferedImage; import java.nio.file.Path; +import java.util.Date; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; import org.kontalk.model.Avatar.UserAvatar; +import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.ChatList; +import org.kontalk.model.message.InMessage; +import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.OutMessage; +import org.kontalk.model.message.ProtoMessage; import org.kontalk.system.Config; import org.kontalk.system.Database; +import org.kontalk.util.ClientUtils; /** * * @author Alexander Bikadorov {@literal } */ public final class Model { + private static final Logger LOGGER = Logger.getLogger(Model.class.getName()); + private static Model INSTANCE = null; private static Path APP_DIR; - private final Path mAppDir; + private static Database DATABASE; private final ContactList mContactList; private final ChatList mChatList; @@ -41,14 +53,26 @@ public final class Model { private UserAvatar mUserAvatar; - public Model(Database db, Path appDir) { - mAppDir = APP_DIR = appDir; + private Model(Database db, Path appDir) { + DATABASE = db; + APP_DIR = appDir; - mAccount = new Account(mAppDir, Config.getInstance()); - mContactList = new ContactList(db); - mChatList = new ChatList(db); + mAccount = new Account(APP_DIR, Config.getInstance()); + mContactList = new ContactList(); + mChatList = new ChatList(); - mUserAvatar = new UserAvatar(mAppDir); + mUserAvatar = new UserAvatar(APP_DIR); + + Avatar.createStorageDir(appDir); + } + + public static Model setup(Database db, Path appDir) { + if (INSTANCE != null) { + LOGGER.warning("already set up"); + return INSTANCE; + } + + return INSTANCE = new Model(db, appDir); } public Account account() { @@ -73,17 +97,59 @@ public void load() { mChatList.load(contactMap); } - public UserAvatar newUserAvatar(BufferedImage image) { - return UserAvatar.create(image, mAppDir); + public UserAvatar setUserAvatar(BufferedImage image) { + return UserAvatar.create(image); + } + + public Optional createInMessage(ProtoMessage protoMessage, + Chat chat, ClientUtils.MessageIDs ids, Optional serverDate) { + InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, + ids.xmppID, serverDate); + + if (newMessage.getID() <= 0) + return Optional.empty(); + + if (chat.getMessages().contains(newMessage)) { + LOGGER.info("message already in chat, dropping this one"); + return Optional.empty(); + } + boolean added = chat.addMessage(newMessage); + if (!added) { + LOGGER.warning("can't add message to chat"); + return Optional.empty(); + } + return Optional.of(newMessage); + } + + public Optional createOutMessage(Chat chat, + List contacts, MessageContent content) { + OutMessage newMessage = new OutMessage(chat, contacts, content, + chat.isSendEncrypted()); + + boolean added = chat.addMessage(newMessage); + if (!added) { + LOGGER.warning("could not add outgoing message to chat"); + return Optional.empty(); + } + return Optional.of(newMessage); } public void deleteUserAvatar() { mUserAvatar.delete(); - mUserAvatar = new UserAvatar(mAppDir); + mUserAvatar = new UserAvatar(APP_DIR); } - // TODO static Path appDir() { + if (APP_DIR == null) + throw new IllegalStateException("model not set up"); + return APP_DIR; } + + public static Database database(){ + if (DATABASE == null) + throw new IllegalStateException("model not set up"); + + return DATABASE; + } } diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 28492ee8..3d06dbf0 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -37,6 +37,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.model.message.KonMessage; import org.kontalk.system.Database; @@ -70,7 +71,6 @@ public abstract class Chat extends Observable implements Observer { COL_GD+" TEXT " + ")"; - private final Database mDB; protected final int mID; private final ChatMessages mMessages; @@ -79,8 +79,7 @@ public abstract class Chat extends Observable implements Observer { private ViewSettings mViewSettings; - protected Chat(Database db, List members, String xmppID, String subject, GroupMetaData gData) { - mDB = db; + protected Chat(List members, String xmppID, String subject, GroupMetaData gData) { mMessages = new ChatMessages(); mRead = true; mViewSettings = new ViewSettings(); @@ -92,18 +91,17 @@ protected Chat(Database db, List members, String xmppID, String subject, mRead, mViewSettings.toJSONString(), Database.setString(gData == null ? "" : gData.toJSON())); - mID = mDB.execInsert(TABLE, values); + mID = Model.database().execInsert(TABLE, values); if (mID < 1) { LOGGER.warning("couldn't insert chat"); return; } - members.stream().forEach(member -> member.insert(mDB, mID)); + members.stream().forEach(member -> member.insert(Model.database(), mID)); } // used when loading from database - protected Chat(Database db, int id, boolean read, String jsonViewSettings) { - mDB = db; + protected Chat(int id, boolean read, String jsonViewSettings) { mID = id; mMessages = new ChatMessages(); mRead = read; @@ -208,7 +206,8 @@ protected void save(List members, String subject) { set.put(COL_READ, mRead); set.put(COL_VIEW_SET, mViewSettings.toJSONString()); - mDB.execUpdate(TABLE, set, mID); + Database db = Model.database(); + db.execUpdate(TABLE, set, mID); // get receiver for this chat List oldMembers = new ArrayList<>(this.getAllMembers()); @@ -216,11 +215,11 @@ protected void save(List members, String subject) { // save new members members.stream() .filter(m -> !oldMembers.contains(m)) - .forEach(m -> m.insert(mDB, mID)); + .forEach(m -> m.insert(db, mID)); oldMembers.removeAll(members); // whats left is too much and can be deleted - oldMembers.stream().forEach(m -> m.delete(mDB)); + oldMembers.stream().forEach(m -> m.delete(db)); } void delete() { @@ -230,15 +229,16 @@ void delete() { return; // members - succ = this.getAllMembers().stream().allMatch(m -> m.delete(mDB)); + succ = this.getAllMembers().stream().allMatch(m -> m.delete(Model.database())); if (!succ) return; // chat itself - mDB.execDelete(TABLE, mID); + Database db = Model.database(); + db.execDelete(TABLE, mID); // all done, commmit deletions - succ = mDB.commit(); + succ = db.commit(); if (!succ) return; @@ -282,13 +282,13 @@ static Optional load(Database db, ResultSet rs, Map cont Chat chat; if (gData != null) { - chat = GroupChat.create(db, id, members, gData, subject, read, jsonViewSettings); + chat = GroupChat.create(id, members, gData, subject, read, jsonViewSettings); } else { if (members.size() != 1) { LOGGER.warning("not one contact for single chat, id="+id); return Optional.empty(); } - chat = new SingleChat(db, id, members.get(0), xmppID, read, jsonViewSettings); + chat = new SingleChat(id, members.get(0), xmppID, read, jsonViewSettings); } chat.loadMessages(db, contactMap); diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 210b5846..950606f5 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -32,6 +32,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.system.Database; /** @@ -41,21 +42,17 @@ public final class ChatList extends Observable implements Observer, Iterable { private static final Logger LOGGER = Logger.getLogger(ChatList.class.getName()); - private final Database mDB; private final Set mChats = Collections.synchronizedSet(new HashSet()); private boolean mUnread = false; - public ChatList(Database db) { - mDB = db; - } - public void load(Map contactMap) { assert mChats.isEmpty(); - try (ResultSet chatRS = mDB.execSelectAll(Chat.TABLE)) { + Database db = Model.database(); + try (ResultSet chatRS = db.execSelectAll(Chat.TABLE)) { while (chatRS.next()) { - Chat chat = Chat.load(mDB, chatRS, contactMap).orElse(null); + Chat chat = Chat.load(db, chatRS, contactMap).orElse(null); if (chat == null) continue; this.putSilent(chat); @@ -108,7 +105,7 @@ public SingleChat getOrCreate(Contact contact, String xmppThreadID) { } private SingleChat createNew(Contact contact, String xmppThreadID) { - SingleChat newChat = new SingleChat(mDB, new Member(contact), xmppThreadID); + SingleChat newChat = new SingleChat(new Member(contact), xmppThreadID); LOGGER.config("new single chat: "+newChat); this.putSilent(newChat); this.changed(newChat); @@ -120,7 +117,7 @@ public GroupChat create(List members, GroupMetaData gData) { } public GroupChat createNew(List members, GroupMetaData gData, String subject) { - GroupChat newChat = GroupChat.create(mDB, members, gData, subject); + GroupChat newChat = GroupChat.create(Model.database(), members, gData, subject); LOGGER.config("new group chat: "+newChat); this.putSilent(newChat); this.changed(newChat); diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index bc6e92a4..06a78939 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -46,8 +46,8 @@ public abstract class GroupChat extends Chat { // TODO overwrite encryption=OFF field private boolean mForceEncryptionOff = false; - private GroupChat(Database db, List members, D gData, String subject) { - super(db, members, "", subject, gData); + private GroupChat(List members, D gData, String subject) { + super(members, "", subject, gData); mGroupData = gData; mSubject = subject; @@ -56,7 +56,7 @@ private GroupChat(Database db, List members, D gData, String subject) { } // used when loading from database - private GroupChat(Database db, + private GroupChat( int id, List members, D gData, @@ -64,7 +64,7 @@ private GroupChat(Database db, boolean read, String jsonViewSettings ) { - super(db, id, read, jsonViewSettings); + super(id, read, jsonViewSettings); mGroupData = gData; mSubject = subject; @@ -224,39 +224,38 @@ public String toString() { } public static final class KonGroupChat extends GroupChat { - private KonGroupChat(Database db, List members, - KonGroupData gData, String subject) { - super(db, members, gData, subject); + private KonGroupChat(List members, KonGroupData gData, String subject) { + super(members, gData, subject); } - private KonGroupChat(Database db, int id, List members, + private KonGroupChat(int id, List members, KonGroupData gData, String subject, boolean read, String jsonViewSettings) { - super(db, id, members, gData, subject, read, jsonViewSettings); + super(id, members, gData, subject, read, jsonViewSettings); } } public static final class MUCChat extends GroupChat { - private MUCChat(Database db, List members, MUCData gData, String subject) { - super(db, members, gData, subject); + private MUCChat(List members, MUCData gData, String subject) { + super(members, gData, subject); } - private MUCChat(Database db, int id, List members, MUCData gData, + private MUCChat(int id, List members, MUCData gData, String subject, boolean read, String jsonViewSettings) { - super(db, id, members, gData, subject, read, jsonViewSettings); + super(id, members, gData, subject, read, jsonViewSettings); } } - static GroupChat create(Database db, int id, List members, + static GroupChat create(int id, List members, GroupMetaData gData, String subject, boolean read, String jsonViewSettings) { return (gData instanceof KonGroupData) ? - new KonGroupChat(db, id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : - new MUCChat(db, id, members, (MUCData) gData, subject, read, jsonViewSettings); + new KonGroupChat(id, members, (KonGroupData) gData, subject, read, jsonViewSettings) : + new MUCChat(id, members, (MUCData) gData, subject, read, jsonViewSettings); } - public static GroupChat create(Database db, List members, GroupMetaData gData, String subject) { + static GroupChat create(Database db, List members, GroupMetaData gData, String subject) { return (gData instanceof KonGroupData) ? - new KonGroupChat(db, members, (KonGroupData) gData, subject) : - new MUCChat(db, members, (MUCData) gData, subject); + new KonGroupChat(members, (KonGroupData) gData, subject) : + new MUCChat(members, (MUCData) gData, subject); } } diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index 6afcf36f..7e4f6ca7 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -25,7 +25,6 @@ import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.model.Contact; -import org.kontalk.system.Database; /** * @@ -37,8 +36,8 @@ public final class SingleChat extends Chat { private final Member mMember; private final String mXMPPID; - SingleChat(Database db, Member member, String xmppID) { - super(db, Arrays.asList(member), xmppID, "", null); + SingleChat(Member member, String xmppID) { + super(Arrays.asList(member), xmppID, "", null); mMember = member; // NOTE: Kontalk Android client is ignoring the chat XMPP-ID @@ -47,14 +46,14 @@ public final class SingleChat extends Chat { } // used when loading from database - SingleChat(Database db, + SingleChat( int id, Member member, String xmppID, boolean read, String jsonViewSettings ) { - super(db, id, read, jsonViewSettings); + super(id, read, jsonViewSettings); mMember = member; mXMPPID = xmppID; diff --git a/src/main/java/org/kontalk/model/message/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java index 75fc579b..68904f12 100644 --- a/src/main/java/org/kontalk/model/message/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -31,7 +31,6 @@ import org.kontalk.model.Contact; import org.kontalk.model.message.MessageContent.Attachment; import org.kontalk.model.message.MessageContent.Preview; -import org.kontalk.system.Database; /** * Model for an XMPP message sent to the user. @@ -42,9 +41,9 @@ public final class InMessage extends KonMessage implements DecryptMessage { private final Transmission mTransmission; - public InMessage(Database db, ProtoMessage proto, Chat chat, JID from, + public InMessage(ProtoMessage proto, Chat chat, JID from, String xmppID, Optional serverDate) { - super(db, + super( chat, xmppID, proto.getContent(), @@ -52,12 +51,12 @@ public InMessage(Database db, ProtoMessage proto, Chat chat, JID from, Status.IN, proto.getCoderStatus()); - mTransmission = new Transmission(db, proto.getContact(), from, mID); + mTransmission = new Transmission(proto.getContact(), from, mID); } // used when loading from database - protected InMessage(Database db, KonMessage.Builder builder) { - super(db, builder); + protected InMessage(KonMessage.Builder builder) { + super(builder); if (builder.mTransmissions.size() != 1) throw new IllegalArgumentException("builder does not contain one transmission"); diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index 55edc686..7567d439 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -38,6 +38,7 @@ import org.kontalk.system.Database; import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.util.EncodingUtils; @@ -106,7 +107,6 @@ public static enum Status { "FOREIGN KEY ("+COL_CHAT_ID+") REFERENCES "+Chat.TABLE+" (_id) " + ")"; - private final Database mDB; protected final int mID; private final Chat mChat; private final String mXMPPID; @@ -121,14 +121,13 @@ public static enum Status { protected CoderStatus mCoderStatus; protected ServerError mServerError; - protected KonMessage(Database db, + protected KonMessage( Chat chat, String xmppID, MessageContent content, Optional serverDate, Status status, CoderStatus coderStatus) { - mDB = db; mChat = chat; mXMPPID = xmppID; mDate = new Date(); @@ -154,15 +153,14 @@ protected KonMessage(Database db, mServerError.toJSON(), mServerDate); - mID = mDB.execInsert(TABLE, values); + mID = Model.database().execInsert(TABLE, values); if (mID <= 0) { LOGGER.log(Level.WARNING, "db, could not insert message"); } } // used when loading from database - protected KonMessage(Database db, Builder builder) { - mDB = db; + protected KonMessage(Builder builder) { mID = builder.mID; mChat = builder.mChat; mXMPPID = builder.mXMPPID; @@ -260,7 +258,7 @@ protected void save() { set.put(COL_COD_ERR, mCoderStatus.getErrors()); set.put(COL_SERV_ERR, Database.setString(mServerError.toJSON())); set.put(COL_SERV_DATE, mServerDate); - mDB.execUpdate(TABLE, set, mID); + Model.database().execUpdate(TABLE, set, mID); } public boolean delete() { @@ -272,7 +270,7 @@ public boolean delete() { LOGGER.warning("not in database: "+this); return true; } - return mDB.execDelete(TABLE, mID); + return Model.database().execDelete(TABLE, mID); } protected void changed(Object arg) { @@ -348,14 +346,14 @@ public static KonMessage load(Database db, ResultSet messageRS, Chat chat, KonMessage.Builder builder = new KonMessage.Builder(id, chat, status, date, content); // TODO one SQL SELECT for each message, performance? looks ok - builder.transmissions(Transmission.load(db, id, contactMap)); + builder.transmissions(Transmission.load(id, contactMap)); builder.xmppID(xmppID); if (serverDate != null) builder.serverDate(serverDate); builder.coderStatus(coderStatus); builder.serverError(serverError); - return builder.build(db); + return builder.build(); } public static final class ServerError { @@ -429,7 +427,7 @@ private Builder(int id, private void coderStatus(CoderStatus coderStatus) { mCoderStatus = coderStatus; } private void serverError(ServerError error) { mServerError = error; } - private KonMessage build(Database db) { + private KonMessage build() { if (mTransmissions == null || mXMPPID == null || mCoderStatus == null || @@ -437,9 +435,9 @@ private KonMessage build(Database db) { throw new IllegalStateException(); if (mStatus == Status.IN) - return new InMessage(db, this); + return new InMessage(this); else - return new OutMessage(db, this); + return new OutMessage(this); } } } diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index c238a64a..eaa66b1e 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -31,7 +31,6 @@ import org.jivesoftware.smack.util.StringUtils; import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; -import org.kontalk.system.Database; /** * Model for an XMPP message from the user to a contact. @@ -42,9 +41,10 @@ public final class OutMessage extends KonMessage { private final Set mTransmissions; - public OutMessage(Database db, Chat chat, List contacts, + public OutMessage(Chat chat, List contacts, MessageContent content, boolean encrypted) { - super(db, chat, + super( + chat, "Kon_" + StringUtils.randomString(8), content, Optional.empty(), @@ -55,7 +55,7 @@ public OutMessage(Database db, Chat chat, List contacts, Set ts = new HashSet<>(); contacts.stream().forEach(contact -> { - boolean succ = ts.add(new Transmission(db, contact, contact.getJID(), mID)); + boolean succ = ts.add(new Transmission(contact, contact.getJID(), mID)); if (!succ) LOGGER.warning("duplicate contact: "+contact); }); @@ -63,8 +63,8 @@ public OutMessage(Database db, Chat chat, List contacts, } // used when loading from database - protected OutMessage(Database db, KonMessage.Builder builder) { - super(db, builder); + protected OutMessage(KonMessage.Builder builder) { + super(builder); mTransmissions = Collections.unmodifiableSet(builder.mTransmissions); } diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index da440c57..ccd398d6 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -34,6 +34,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.kontalk.model.Contact; +import org.kontalk.model.Model; import org.kontalk.system.Database; /** @@ -62,15 +63,13 @@ final public class Transmission { "FOREIGN KEY ("+COL_CONTACT_ID+") REFERENCES "+Contact.TABLE+" (_id) " + ")"; - private final Database mDB; private final int mID; private final Contact mContact; private final JID mJID; private Date mReceivedDate; - Transmission(Database db, Contact contact, JID jid, int messageID) { - mDB = db; + Transmission(Contact contact, JID jid, int messageID) { mContact = contact; mJID = jid; mReceivedDate = null; @@ -79,7 +78,6 @@ final public class Transmission { } private Transmission(Database db, int id, Contact contact, JID jid, Date receivedDate) { - mDB = db; mID = id; mContact = contact; mJID = jid; @@ -114,7 +112,7 @@ private int insert(int messageID) { mJID, mReceivedDate); - int id = mDB.execInsert(TABLE, values); + int id = Model.database().execInsert(TABLE, values); if (id <= 0) { LOGGER.log(Level.WARNING, "could not insert"); return -2; @@ -125,7 +123,7 @@ private int insert(int messageID) { private void save() { Map set = new HashMap<>(); set.put(COL_REC_DATE, mReceivedDate); - mDB.execUpdate(TABLE, set, mID); + Model.database().execUpdate(TABLE, set, mID); } boolean delete() { @@ -133,7 +131,7 @@ boolean delete() { LOGGER.warning("not in database: "+this); return true; } - return mDB.execDelete(TABLE, mID); + return Model.database().execDelete(TABLE, mID); } @Override @@ -141,7 +139,8 @@ public String toString() { return "T:id="+mID+",contact="+mContact+",jid="+mJID+",recdate="+mReceivedDate; } - static Set load(Database db, int messageID, Map contactMap) { + static Set load(int messageID, Map contactMap) { + Database db = Model.database(); HashSet ts = new HashSet<>(); try (ResultSet transmissionRS = db.execSelectWhereInsecure(TABLE, COL_MESSAGE_ID + " == " + messageID)) { diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index f2faa887..36769ba2 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -19,7 +19,6 @@ package org.kontalk.system; import java.awt.image.BufferedImage; -import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; @@ -46,11 +45,9 @@ public final class AvatarHandler { private final Client mClient; private final Model mModel; - AvatarHandler(Client client, Model model, Path appDir) { + AvatarHandler(Client client, Model model) { mClient = client; mModel = model; - - Avatar.createDir(appDir); } public void onNotify(JID jid, String id) { diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 61c59f0d..253bca45 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -107,13 +107,13 @@ public Control(Path appDir) throws KonException { throw ex; } - mModel = new Model(mDB, appDir); + mModel = Model.setup(mDB, appDir); mClient = Client.create(this, appDir); mChatStateManager = new ChatStateManager(mClient); mAttachmentManager = AttachmentManager.create(this, appDir); mRosterHandler = new RosterHandler(this, mClient, mModel); - mAvatarHandler = new AvatarHandler(mClient, mModel, appDir); + mAvatarHandler = new AvatarHandler(mClient, mModel); mGroupControl = new GroupControl(this, mModel); } @@ -258,23 +258,11 @@ public void onNewInMessage(MessageIDs ids, return; } - // TODO move - InMessage newMessage = new InMessage(mDB, protoMessage, chat, ids.jid, - ids.xmppID, serverDate); - - if (newMessage.getID() <= 0) + InMessage newMessage = mModel.createInMessage( + protoMessage, chat, ids, serverDate).orElse(null); + if (newMessage == null) return; - if (chat.getMessages().contains(newMessage)) { - LOGGER.info("message already in chat, dropping this one"); - return; - } - boolean added = chat.addMessage(newMessage); - if (!added) { - LOGGER.warning("can't add message to chat"); - return; - } - GroupCommand com = newMessage.getContent().getGroupCommand().orElse(null); if (com != null) { if (chat instanceof GroupChat) { @@ -415,15 +403,13 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { return false; } - // TODO move - OutMessage newMessage = new OutMessage(mDB, chat, contacts, content, - chat.isSendEncrypted()); + OutMessage newMessage = mModel.createOutMessage( + chat, contacts, content).orElse(null); + if (newMessage == null) + return false; + if (newMessage.getContent().getAttachment().isPresent()) mAttachmentManager.createImagePreview(newMessage); - boolean added = chat.addMessage(newMessage); - if (!added) { - LOGGER.warning("could not add outgoing message to chat"); - } return this.sendMessage(newMessage); } @@ -802,7 +788,7 @@ public void sendAttachment(Chat chat, Path file){ } public void setUserAvatar(BufferedImage image) { - Avatar.UserAvatar newAvatar = mModel.newUserAvatar(image); + Avatar.UserAvatar newAvatar = mModel.setUserAvatar(image); byte[] avatarData = newAvatar.imageData().orElse(null); if (avatarData == null || newAvatar.getID().isEmpty()) return; From 528b40e54666c5baef208c4855ebd28e8396ffc3 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 5 Apr 2016 16:16:39 +0200 Subject: [PATCH 209/257] persistence: new module for database and configuration --- src/main/java/org/kontalk/client/Client.java | 2 +- src/main/java/org/kontalk/model/Account.java | 4 ++-- src/main/java/org/kontalk/model/Contact.java | 2 +- .../java/org/kontalk/model/ContactList.java | 2 +- src/main/java/org/kontalk/model/Model.java | 4 ++-- src/main/java/org/kontalk/model/chat/Chat.java | 2 +- .../java/org/kontalk/model/chat/ChatList.java | 2 +- .../org/kontalk/model/chat/ChatMessages.java | 2 +- .../java/org/kontalk/model/chat/GroupChat.java | 2 +- .../java/org/kontalk/model/chat/Member.java | 2 +- .../org/kontalk/model/message/KonMessage.java | 2 +- .../kontalk/model/message/Transmission.java | 2 +- .../{system => persistence}/Config.java | 18 +++++++----------- .../{system => persistence}/Database.java | 8 +++----- .../org/kontalk/system/AttachmentManager.java | 1 + .../java/org/kontalk/system/AvatarHandler.java | 1 + .../org/kontalk/system/ChatStateManager.java | 1 + src/main/java/org/kontalk/system/Control.java | 2 ++ .../java/org/kontalk/system/RosterHandler.java | 1 + .../java/org/kontalk/view/ChatListView.java | 2 +- src/main/java/org/kontalk/view/ChatView.java | 2 +- .../java/org/kontalk/view/ComponentUtils.java | 2 +- .../org/kontalk/view/ConfigurationDialog.java | 2 +- src/main/java/org/kontalk/view/MainFrame.java | 2 +- .../java/org/kontalk/view/ProfileDialog.java | 2 +- .../java/org/kontalk/view/TrayManager.java | 2 +- src/main/java/org/kontalk/view/Utils.java | 2 +- src/main/java/org/kontalk/view/View.java | 2 +- 28 files changed, 39 insertions(+), 39 deletions(-) rename src/main/java/org/kontalk/{system => persistence}/Config.java (92%) rename src/main/java/org/kontalk/{system => persistence}/Database.java (98%) diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index b277a49e..18b3946f 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -52,7 +52,7 @@ import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.pubsub.packet.PubSub; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.misc.KonException; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.JID; diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index e2d22d39..a8ab0d53 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -40,10 +40,10 @@ import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; import org.kontalk.misc.JID; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; /** - * The user account. There can only be one. + * The user account. * * @author Alexander Bikadorov {@literal } */ diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index dfbbbfcf..7a9c21f6 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -31,7 +31,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.packet.Presence; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; import org.kontalk.util.EncodingUtils; import org.kontalk.util.XMPPUtils; diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index e9c80bf2..6f1861e0 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -30,7 +30,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * Global list of all contacts. diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index 6808d7b4..253d904c 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -32,8 +32,8 @@ import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.OutMessage; import org.kontalk.model.message.ProtoMessage; -import org.kontalk.system.Config; -import org.kontalk.system.Database; +import org.kontalk.persistence.Config; +import org.kontalk.persistence.Database; import org.kontalk.util.ClientUtils; /** diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 3d06dbf0..4b324472 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -39,7 +39,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.Model; import org.kontalk.model.message.KonMessage; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * A model for a conversation thread consisting of an ordered list of messages. diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index 950606f5..a83abadf 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -33,7 +33,7 @@ import java.util.logging.Logger; import org.kontalk.model.Contact; import org.kontalk.model.Model; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * The global list of all chats. diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index b5590cac..1c0042a7 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -35,7 +35,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.message.KonMessage; import org.kontalk.model.message.OutMessage; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * All messages of a chat. diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 06a78939..7e6b9a1e 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -29,7 +29,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.chat.GroupMetaData.MUCData; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * A long-term persistent chat conversation with multiple participants. diff --git a/src/main/java/org/kontalk/model/chat/Member.java b/src/main/java/org/kontalk/model/chat/Member.java index 74055b80..5698064c 100644 --- a/src/main/java/org/kontalk/model/chat/Member.java +++ b/src/main/java/org/kontalk/model/chat/Member.java @@ -31,7 +31,7 @@ import java.util.logging.Logger; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.model.Contact; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * A contact association with a chat. diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index 7567d439..73204c69 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -35,7 +35,7 @@ import java.util.logging.Logger; import org.json.simple.JSONObject; import org.json.simple.JSONValue; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; import org.kontalk.model.Model; diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index ccd398d6..31e8b107 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -35,7 +35,7 @@ import java.util.logging.Logger; import org.kontalk.model.Contact; import org.kontalk.model.Model; -import org.kontalk.system.Database; +import org.kontalk.persistence.Database; /** * A transmission of one message. diff --git a/src/main/java/org/kontalk/system/Config.java b/src/main/java/org/kontalk/persistence/Config.java similarity index 92% rename from src/main/java/org/kontalk/system/Config.java rename to src/main/java/org/kontalk/persistence/Config.java index 4b4189d5..86e8f27e 100644 --- a/src/main/java/org/kontalk/system/Config.java +++ b/src/main/java/org/kontalk/persistence/Config.java @@ -16,12 +16,11 @@ * along with this program. If not, see . */ -package org.kontalk.system; +package org.kontalk.persistence; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.configuration.ConfigurationException; @@ -29,7 +28,6 @@ import org.kontalk.util.Tr; /** - * * Global configuration options. * * @author Alexander Bikadorov {@literal } @@ -82,7 +80,7 @@ private Config(Path configFile) { try { this.load(configFile.toString()); } catch (ConfigurationException ex) { - LOGGER.info("Configuration not found. Using default values"); + LOGGER.info("configuration file not found; using default values"); } this.setFileName(configFile.toString()); @@ -111,11 +109,9 @@ private Config(Path configFile) { map.put(MAIN_TRAY_CLOSE, false); map.put(MAIN_ENTER_SENDS, true); - for(Entry e : map.entrySet()) { - if (!this.containsKey(e.getKey())) { - this.setProperty(e.getKey(), e.getValue()); - } - } + map.entrySet().stream() + .filter(e -> !this.containsKey(e.getKey())) + .forEach(e -> this.setProperty(e.getKey(), e.getValue())); } public void saveToFile() { @@ -126,7 +122,7 @@ public void saveToFile() { } } - static void initialize(Path appDir) { + public static void initialize(Path appDir) { if (INSTANCE != null) { LOGGER.warning("already initialized"); return; @@ -135,7 +131,7 @@ static void initialize(Path appDir) { INSTANCE = new Config(appDir.resolve(Config.FILENAME)); } - public synchronized static Config getInstance() { + public static Config getInstance() { if (INSTANCE == null) throw new IllegalStateException("not initialized"); diff --git a/src/main/java/org/kontalk/system/Database.java b/src/main/java/org/kontalk/persistence/Database.java similarity index 98% rename from src/main/java/org/kontalk/system/Database.java rename to src/main/java/org/kontalk/persistence/Database.java index 3f027219..b62b2cda 100644 --- a/src/main/java/org/kontalk/system/Database.java +++ b/src/main/java/org/kontalk/persistence/Database.java @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.kontalk.system; +package org.kontalk.persistence; import java.nio.file.Path; import java.sql.Connection; @@ -59,8 +59,6 @@ public final class Database { private static final Logger LOGGER = Logger.getLogger(Database.class.getName()); - private static Database INSTANCE = null; - public static final String SQL_ID = "_id INTEGER PRIMARY KEY AUTOINCREMENT, "; private static final String FILENAME = "kontalk_db.sqlite"; @@ -71,7 +69,7 @@ public final class Database { private Connection mConn = null; - Database(Path appDir) throws KonException { + public Database(Path appDir) throws KonException { // load the sqlite-JDBC driver using the current class loader try { Class.forName("org.sqlite.JDBC"); @@ -193,7 +191,7 @@ private void update(int fromVersion) throws SQLException { LOGGER.info("updated to version "+DB_VERSION); } - synchronized void close() { + public synchronized void close() { try { if(mConn == null || mConn.isClosed()) return; diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 67d8363f..edec8811 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.persistence.Config; import java.awt.Dimension; import java.awt.Image; import java.awt.image.BufferedImage; diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 36769ba2..5df3471c 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.persistence.Config; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.List; diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index 86851448..ec3d32bb 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.persistence.Config; import java.util.Map; import java.util.Timer; import java.util.TimerTask; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 253bca45..dba5ccd9 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -18,6 +18,8 @@ package org.kontalk.system; +import org.kontalk.persistence.Config; +import org.kontalk.persistence.Database; import org.kontalk.model.Account; import java.awt.image.BufferedImage; import java.nio.file.Path; diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 2b4794be..4c8757bd 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -18,6 +18,7 @@ package org.kontalk.system; +import org.kontalk.persistence.Config; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 70691a94..3b7ddd8c 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -34,7 +34,7 @@ import javax.swing.ListSelectionModel; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.ChatList; diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 47281bdb..1baa6467 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -76,7 +76,7 @@ import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.system.Control; import org.kontalk.util.EncodingUtils; import org.kontalk.util.MediaUtils; diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 8811bde8..3b30c4b4 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -91,7 +91,7 @@ import org.kontalk.model.Model; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index eaa7a311..29a83bd9 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -51,7 +51,7 @@ import javax.swing.JFrame; import javax.swing.text.NumberFormatter; import org.apache.commons.lang.StringUtils; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.KonException; import org.kontalk.model.Account; diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index b10a3ff7..9e5a2c3a 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -54,7 +54,7 @@ import static javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.Kontalk; import org.kontalk.model.Model; import org.kontalk.system.Control; diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 5e9b4ea1..6e275235 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -49,7 +49,7 @@ import org.apache.commons.lang.ObjectUtils; import org.kontalk.client.Client; import org.kontalk.model.Model; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.system.Control; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; diff --git a/src/main/java/org/kontalk/view/TrayManager.java b/src/main/java/org/kontalk/view/TrayManager.java index 212f1855..40a0a586 100644 --- a/src/main/java/org/kontalk/view/TrayManager.java +++ b/src/main/java/org/kontalk/view/TrayManager.java @@ -36,7 +36,7 @@ import java.util.logging.Logger; import javax.swing.SwingUtilities; import org.kontalk.model.Model; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.util.Tr; /** diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 16a81dbb..9aaac72b 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -62,7 +62,7 @@ import org.kontalk.model.Contact; import org.kontalk.model.ContactList; import org.kontalk.model.chat.Member; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; import org.ocpsoft.prettytime.PrettyTime; diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index ce8614bf..141eb9e7 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -44,7 +44,7 @@ import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.kontalk.client.Client; -import org.kontalk.system.Config; +import org.kontalk.persistence.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; From 88558fa83ad3e0847ad378653e56329c9c091bdb Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 13 Apr 2016 18:06:20 +0200 Subject: [PATCH 210/257] view: fixing inf.loop when searching in contact list --- .../java/org/kontalk/view/ChatListView.java | 33 +++---------------- .../org/kontalk/view/ContactListView.java | 21 ++++-------- src/main/java/org/kontalk/view/ListView.java | 30 +++++++++++++++++ 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 3b7ddd8c..e6d29c9f 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -32,8 +32,6 @@ import java.util.Timer; import javax.swing.Box; import javax.swing.ListSelectionModel; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; import org.kontalk.persistence.Config; import org.kontalk.model.message.KonMessage; import org.kontalk.model.chat.Chat; @@ -60,32 +58,6 @@ final class ChatListView extends ListView { this.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - // actions triggered by selection - this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - - Chat lastChat = null; - - @Override - public void valueChanged(ListSelectionEvent e) { - if (e.getValueIsAdjusting()) - return; - - Chat chat = ChatListView.this.getSelectedValue().orElse(null); - if (chat == null) { - // note: this happens also on righ-click for some reason - return; - } - - // if event is caused by filtering, dont do anything - if (lastChat == chat) - return; - - mView.clearSearch(); - mView.selectedChatChanged(chat); - lastChat = chat; - } - }); - this.updateOnEDT(null); } @@ -122,6 +94,11 @@ private void deleteChat(ChatItem item) { mView.getControl().deleteChat(chatItem.mValue); } + @Override + protected void selectionChanged(Chat value) { + mView.selectedChatChanged(value); + } + @Override protected WebPopupMenu rightClickMenu(ChatItem item) { WebPopupMenu menu = new WebPopupMenu(); diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 8d6fed2d..afdc916f 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -33,8 +33,6 @@ import java.awt.event.MouseEvent; import java.util.Observer; import javax.swing.Box; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.StringEscapeUtils; import org.kontalk.model.Contact; import org.kontalk.model.Model; @@ -43,7 +41,7 @@ import org.kontalk.view.ContactListView.ContactItem; /** - * Display all contact (aka contacts) in a brief list. + * Display all contacts in a brief list. * @author Alexander Bikadorov {@literal } */ final class ContactListView extends ListView implements Observer { @@ -55,18 +53,6 @@ final class ContactListView extends ListView implements Ob mModel = model; - // actions triggered by selection - this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - Contact contact = ContactListView.this.getSelectedValue().orElse(null); - if (contact == null) - return; - - mView.showContactDetails(contact); - } - }); - // actions triggered by mouse events this.addMouseListener(new MouseAdapter() { @Override @@ -92,6 +78,11 @@ protected ContactItem newItem(Contact value) { return new ContactItem(value); } + @Override + protected void selectionChanged(Contact value) { + mView.showContactDetails(value); + } + @Override protected WebPopupMenu rightClickMenu(ContactItem item) { WebPopupMenu menu = new WebPopupMenu(); diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index dae5240e..3184f55e 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -53,6 +53,8 @@ import javax.swing.RowSorter; import javax.swing.SortOrder; import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableRowSorter; @@ -124,6 +126,32 @@ public boolean include(Entry ent // use custom renderer this.setDefaultRenderer(TableItem.class, new TableRenderer()); + // actions triggered by selection + this.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + + private V lastValue = null; + + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) + return; + + V value = ListView.this.getSelectedValue().orElse(null); + if (value == null) { + // note: this happens also on right-click for some reason + return; + } + // if event is caused by filtering, dont do anything + if (lastValue == value) + return; + + lastValue = value; + mView.clearSearch(); + + ListView.this.selectionChanged(value); + } + }); + // trigger editing to forward mouse events this.addMouseMotionListener(new MouseMotionListener() { @Override @@ -193,6 +221,8 @@ private void showPopupMenu(MouseEvent e, I item) { menu.show(this, e.getX(), e.getY()); } + protected void selectionChanged(V value){}; + protected abstract WebPopupMenu rightClickMenu(I item); @SuppressWarnings("unchecked") From c501d1b591de41165dd4a00dad5f7c5ed0bc3be7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 18 Apr 2016 17:38:05 +0200 Subject: [PATCH 211/257] keep up-to-date with client-common-java --- client-common-java | 2 +- src/main/java/org/kontalk/crypto/Decryptor.java | 16 +++++++++------- src/main/java/org/kontalk/crypto/Encryptor.java | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/client-common-java b/client-common-java index 815ce670..ad48809b 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit 815ce6703fe6fc9678440f9932e395eef956081e +Subproject commit ad48809b66cbb32bc3e4cc3249150095253fdbfc diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index c0118bc3..6bcbe1ad 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -28,13 +28,13 @@ import java.io.OutputStream; import java.nio.file.Path; import java.text.ParseException; +import java.util.Arrays; import java.util.EnumSet; import java.util.Iterator; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; import org.apache.http.util.EncodingUtils; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -339,7 +339,7 @@ private static DecryptionResult verifySignature(DecryptionResult result, * The decrypted content of a message is in CPIM format. */ private static MessageContent parseCPIMOrNull(DecryptMessage message, String cpim, - String myUid, Optional senderKeyUID) { + String myUID, Optional senderKeyUID) { CPIMMessage cpimMessage; try { @@ -361,15 +361,17 @@ private static MessageContent parseCPIMOrNull(DecryptMessage message, String cpi // LOGGER.warning("MIME type mismatch"); //} - // check that the recipient matches the full uid of the personal key - if (!StringUtils.defaultString(cpimMessage.getTo()).contains(myUid)) { - LOGGER.warning("destination does not match personal key"); + // check that the recipient matches the full UID of the personal key + + if (!Arrays.asList(cpimMessage.getTo()).stream() + .anyMatch(s -> s.contains(myUID))) { + LOGGER.warning("receiver list does not include own UID"); errors.add(Coder.Error.INVALID_RECIPIENT); } - // check that the sender matches the full uid of the sender's key + // check that the sender matches the full UID of the sender's key if (senderKeyUID.isPresent() && !senderKeyUID.get().equals(cpimMessage.getFrom())) { - LOGGER.warning("sender doesn't match sender's key"); + LOGGER.warning("sender does not match UID in public key of sender"); errors.add(Coder.Error.INVALID_SENDER); } diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index 4fbdf1cb..5cd4b848 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -93,11 +93,11 @@ private Optional encryptData(String data, String mime) { // secure the message against replay attacks using Message/CPIM String from = myKey.getUserId(); - String to = receiverKeys.stream() + String[] tos = receiverKeys.stream() .map(key -> key.userID) - .collect(Collectors.joining("; ")); + .toArray(String[]::new); - CPIMMessage cpim = new CPIMMessage(from, to, new Date(), mime, data); + CPIMMessage cpim = new CPIMMessage(from, tos, new Date(), mime, data); byte[] plainText; try { plainText = cpim.toByteArray(); From 242cf782c2cb0543a64022dd1012fc632f7b68ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Suso=20Comesa=C3=B1a?= Date: Tue, 19 Apr 2016 14:13:28 +0000 Subject: [PATCH 212/257] Translated using Weblate (Spanish) Currently translated at 100.0% (259 of 259 strings) --- src/main/resources/i18n/strings_es.properties | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/resources/i18n/strings_es.properties b/src/main/resources/i18n/strings_es.properties index 4ced5a48..7f8354db 100644 --- a/src/main/resources/i18n/strings_es.properties +++ b/src/main/resources/i18n/strings_es.properties @@ -298,3 +298,37 @@ s_KRQ6=Introduzca la contraseña… s_2R2P=cargando… s_307W=bajando… s_4WC0=No se pudo validar el certificado del servidor. +s_68LN=Solicitud +s_7MDX=Estado de la solicitud de autorización del contacto +s_OTEU=Conceder la autorización automáticamente +s_MFZY=Conceder automáticamente peticiones de autorización de estado on-line de otros usuarios +s_FAS6=Enviar actividad de conversación (escribiendo, ...) a otros usuarios +s_29CW=Editar contacto +s_AGYA=Editar la configuración del contacto +s_MNIJ=Esperando... +s_0WUC=No verificado +s_9S4L=Solicitud de autorización +s_FDTC=Al aceptar, este contacto podrá ver su estado de conexión. +s_TAG1=Perfil del usuario +s_84VP=Edite su perfil +s_4QXX=Su foto de perfil: +s_1R7I=Perfil +s_O2UH=Configurar su perfil de usuario +s_6SC9=Clave ID de usuario: +s_6K2I=Descargar fotos de perfil +s_08GL=Descargar las imágenes de perfil del contacto +s_T2TO=Mostrarse a los contactos +s_F1X0=Mostrarse en la lista de contactos +s_3PFR=Retirar +s_D7FH=No soportado por el servidor +s_WUDV=Propietario del grupo +s_RCDY=Red +s_LPE1=Configuración de la red +s_R4F8=Original +s_K83J=Pequeño (0.3MP) +s_RAMV=Medio (0.5MP) +s_7CPG=Grande (0.8MP) +s_5RTB=Reducir el tamaño de las imágenes antes de enviar +s_13FX=Cambiar el tamaño de las imágenes adjuntas: +s_8OBK=Enviar archivo +s_7YWF=max. tamaño: From 9498e45f9139a5f8cffe6102fb63206a73acbcad Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 19 Apr 2016 18:45:51 +0200 Subject: [PATCH 213/257] client: extended service discovery for finding server item features --- src/main/java/org/kontalk/client/Client.java | 48 +------ .../org/kontalk/client/FeatureDiscovery.java | 119 ++++++++++++++++++ src/main/java/org/kontalk/misc/ViewEvent.java | 6 +- src/main/java/org/kontalk/system/Control.java | 5 +- src/main/java/org/kontalk/view/ChatView.java | 6 +- .../java/org/kontalk/view/ProfileDialog.java | 4 +- src/main/java/org/kontalk/view/View.java | 10 +- 7 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 src/main/java/org/kontalk/client/FeatureDiscovery.java diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 18b3946f..5e57d174 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -23,9 +23,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; @@ -44,14 +42,11 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.RosterEntry; -import org.jivesoftware.smackx.address.packet.MultipleAddresses; import org.jivesoftware.smackx.caps.EntityCapsManager; import org.jivesoftware.smackx.caps.cache.SimpleDirectoryPersistentCache; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; -import org.jivesoftware.smackx.disco.packet.DiscoverInfo; -import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.kontalk.persistence.Config; import org.kontalk.misc.KonException; import org.kontalk.crypto.PersonalKey; @@ -70,28 +65,19 @@ public final class Client implements StanzaListener, Runnable { private static final String CAPS_CACHE_DIR = "caps_cache"; private static final LinkedBlockingQueue TASK_QUEUE = new LinkedBlockingQueue<>(); - private static final Map FEATURE_MAP; public enum PresenceCommand {REQUEST, GRANT, DENY}; - public enum ServerFeature {USER_AVATAR, ATTACHMENT_UPLOAD, MULTI_ADDRESSING} private enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; - private final EnumSet mFeatures; + private final EnumSet mFeatures; private KonConnection mConn = null; private AvatarSendReceiver mAvatarSendReceiver = null; - static { - FEATURE_MAP = new HashMap<>(); - FEATURE_MAP.put(PubSub.NAMESPACE, ServerFeature.USER_AVATAR); - FEATURE_MAP.put(HTTPFileClient.KON_UPLOAD_FEATURE, ServerFeature.ATTACHMENT_UPLOAD); - FEATURE_MAP.put(MultipleAddresses.NAMESPACE, ServerFeature.MULTI_ADDRESSING); - } - private Client(Control control, Path appDir) { mControl = control; //mLimited = limited; @@ -101,7 +87,7 @@ private Client(Control control, Path appDir) { // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; - mFeatures = EnumSet.noneOf(ServerFeature.class); + mFeatures = EnumSet.noneOf(FeatureDiscovery.Feature.class); // setting caps cache File cacheDir = appDir.resolve(CAPS_CACHE_DIR).toFile(); @@ -220,30 +206,8 @@ private void connectAsync() { } } - // (server) service discovery, XEP-0030 - // NOTE: smack automatically creates instances of SDM and CapsM and connects them - ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(mConn); - DiscoverInfo info = null; - try { - // blocking - // NOTE: null parameter does not work - info = discoManager.discoverInfo(mConn.getServiceName()); - } catch (SmackException.NoResponseException | - XMPPException.XMPPErrorException | - SmackException.NotConnectedException ex) { - LOGGER.log(Level.WARNING, "can't get service discovery info"); - } - mFeatures.clear(); - if (info != null) { - for (DiscoverInfo.Feature feature: info.getFeatures()) { - String var = feature.getVar(); - if (FEATURE_MAP.containsKey(var)) { - mFeatures.add(FEATURE_MAP.get(var)); - } - } - } - LOGGER.info("supported server features: "+mFeatures); + mFeatures.addAll(FeatureDiscovery.discover(mConn)); // Caps, XEP-0115 // NOTE: caps manager is automatically used by Smack @@ -481,7 +445,7 @@ public void publishAvatar(String id, byte[] data) { LOGGER.warning("no avatar sender"); return; } - if (mFeatures.contains(Client.ServerFeature.USER_AVATAR)) { + if (mFeatures.contains(FeatureDiscovery.Feature.USER_AVATAR)) { mAvatarSendReceiver.publish(id, data); } else { LOGGER.warning("not supported by server"); @@ -494,7 +458,7 @@ public boolean deleteAvatar() { return false; } - if (mFeatures.contains(Client.ServerFeature.USER_AVATAR)) { + if (mFeatures.contains(FeatureDiscovery.Feature.USER_AVATAR)) { return mAvatarSendReceiver.delete(); } else { LOGGER.warning("not supported by server"); @@ -516,7 +480,7 @@ void newException(KonException konException) { } String multiAddressHost() { - return mFeatures.contains(Client.ServerFeature.MULTI_ADDRESSING) + return mFeatures.contains(FeatureDiscovery.Feature.MULTI_ADDRESSING) && mConn != null ? mConn.getHost() : ""; } diff --git a/src/main/java/org/kontalk/client/FeatureDiscovery.java b/src/main/java/org/kontalk/client/FeatureDiscovery.java new file mode 100644 index 00000000..570e162f --- /dev/null +++ b/src/main/java/org/kontalk/client/FeatureDiscovery.java @@ -0,0 +1,119 @@ +/* + * Kontalk Java client + * Copyright (C) 2016 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.address.packet.MultipleAddresses; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; +import org.jivesoftware.smackx.disco.packet.DiscoverItems; +import org.jivesoftware.smackx.pubsub.packet.PubSub; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +public final class FeatureDiscovery { + private static final Logger LOGGER = Logger.getLogger(FeatureDiscovery.class.getName()); + + private static final Map FEATURE_MAP; + + public enum Feature { + USER_AVATAR, + MULTI_ADDRESSING, + /** Old Kontalk upload service. */ + KON_FILE_UPLOAD, + /** New XEP-0363 upload service. */ + HTTP_FILE_UPLOAD + } + + static { + FEATURE_MAP = new HashMap<>(); + FEATURE_MAP.put(PubSub.NAMESPACE, Feature.USER_AVATAR); + FEATURE_MAP.put(HTTPFileClient.KON_UPLOAD_FEATURE, Feature.KON_FILE_UPLOAD); + FEATURE_MAP.put(MultipleAddresses.NAMESPACE, Feature.MULTI_ADDRESSING); + FEATURE_MAP.put(HTTPFileUpload.NAMESPACE, Feature.HTTP_FILE_UPLOAD); + } + + /** (server) service discovery, XEP-0030. */ + static EnumSet discover(XMPPConnection conn) { + // NOTE: smack automatically creates instances of SDM and CapsM and connects them + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(conn); + + // 1. get features from server + EnumSet features = discover(discoManager, conn.getServiceName()); + + DiscoverItems items = null; + try { + items = discoManager.discoverItems(conn.getServiceName()); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't get service discovery items", ex); + return features; + } + + // 2. get features from server items + for (DiscoverItems.Item item: items.getItems()) { + features.addAll(discover(discoManager, item.getEntityID())); + } + + LOGGER.info("supported server features: "+features); + return features; + } + + private static EnumSet discover(ServiceDiscoveryManager dm, String entity) { + EnumSet features = EnumSet.noneOf(Feature.class); + DiscoverInfo info; + try { + // blocking + // NOTE: null parameter does not work + info = dm.discoverInfo(entity); + } catch (SmackException.NoResponseException | + XMPPException.XMPPErrorException | + SmackException.NotConnectedException ex) { + LOGGER.log(Level.WARNING, "can't get service discovery info", ex); + return features; + } + + List identities = info.getIdentities(); + LOGGER.config("entity: " + entity + " identities: " + + identities.stream() + .map(i -> i.toXML()) + .collect(Collectors.toList())); + + for (DiscoverInfo.Feature feature: info.getFeatures()) { + String var = feature.getVar(); + if (FEATURE_MAP.containsKey(var)) { + features.add(FEATURE_MAP.get(var)); + } + } + + return features; + } +} diff --git a/src/main/java/org/kontalk/misc/ViewEvent.java b/src/main/java/org/kontalk/misc/ViewEvent.java index 0ec4fe8e..62ada4b5 100644 --- a/src/main/java/org/kontalk/misc/ViewEvent.java +++ b/src/main/java/org/kontalk/misc/ViewEvent.java @@ -19,7 +19,7 @@ package org.kontalk.misc; import java.util.EnumSet; -import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.crypto.PGPUtils.PGPCoderKey; import org.kontalk.model.message.InMessage; import org.kontalk.model.message.KonMessage; @@ -38,9 +38,9 @@ private ViewEvent() {} /** Application status changed. */ public static class StatusChange extends ViewEvent { public final Control.Status status; - public final EnumSet features; + public final EnumSet features; - public StatusChange(Control.Status status, EnumSet features) { + public StatusChange(Control.Status status, EnumSet features) { this.status = status; this.features = features; } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index dba5ccd9..ac7afe99 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -38,6 +38,7 @@ import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.chatstates.ChatState; import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.client.KonMessageSender; import org.kontalk.crypto.Coder; import org.kontalk.crypto.PGPUtils; @@ -154,7 +155,7 @@ public void shutDown(boolean exit) { mViewControl.disconnect(); mViewControl.changed(new ViewEvent.StatusChange(Status.SHUTTING_DOWN, - EnumSet.noneOf(Client.ServerFeature.class))); + EnumSet.noneOf(FeatureDiscovery.Feature.class))); try { mDB.close(); } catch (RuntimeException ex) { @@ -183,7 +184,7 @@ ViewControl getViewControl() { /* events from network client */ - public void onStatusChange(Status status, EnumSet features) { + public void onStatusChange(Status status, EnumSet features) { mViewControl.changed(new ViewEvent.StatusChange(status, features)); if (status == Status.CONNECTED) { diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 1baa6467..2a40dda1 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -72,7 +72,7 @@ import org.apache.commons.io.FileUtils; import org.apache.tika.Tika; import org.jivesoftware.smackx.chatstates.ChatState; -import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.system.AttachmentManager; @@ -371,12 +371,12 @@ public void keyTyped(KeyEvent e) { mSendButton.addHotkey(sendHotkey, TooltipWay.up); } - void onStatusChange(Control.Status status, EnumSet serverFeature) { + void onStatusChange(Control.Status status, EnumSet serverFeature) { Boolean supported = null; switch(status) { case CONNECTED: this.setColor(Color.WHITE); - supported = serverFeature.contains(Client.ServerFeature.ATTACHMENT_UPLOAD); + supported = serverFeature.contains(FeatureDiscovery.Feature.KON_FILE_UPLOAD); break; case DISCONNECTED: case ERROR: diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 6e275235..838b5607 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -47,7 +47,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang.ObjectUtils; -import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.model.Model; import org.kontalk.persistence.Config; import org.kontalk.system.Control; @@ -98,7 +98,7 @@ final class ProfileDialog extends WebDialog { //setTransferHandler ( new ImageDragHandler ( image1, i1 ) ); // permanent, user has to re-open the dialog on change - final boolean supported = mView.serverFeatures().contains(Client.ServerFeature.USER_AVATAR); + final boolean supported = mView.serverFeatures().contains(FeatureDiscovery.Feature.USER_AVATAR); mAvatarImage.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index 141eb9e7..ebd8d8b0 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -43,7 +43,7 @@ import java.util.EnumSet; import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.persistence.Config; import org.kontalk.misc.ViewEvent; import org.kontalk.model.chat.Chat; @@ -108,7 +108,7 @@ public final class View implements Observer { final String tr_not_supported = Tr.tr("Not supported by server"); private Control.Status mCurrentStatus; - private EnumSet mServerFeatures; + private EnumSet mServerFeatures; private View(ViewControl control, Model model) { mControl = control; @@ -149,7 +149,7 @@ private View(ViewControl control, Model model) { this.setHotkeys(); - this.statusChanged(Control.Status.DISCONNECTED, EnumSet.noneOf(Client.ServerFeature.class)); + this.statusChanged(Control.Status.DISCONNECTED, EnumSet.noneOf(FeatureDiscovery.Feature.class)); mMainFrame.setVisible(true); } @@ -195,7 +195,7 @@ Control.Status currentStatus() { return mCurrentStatus; } - EnumSet serverFeatures() { + EnumSet serverFeatures() { return mServerFeatures; } @@ -257,7 +257,7 @@ private void updateOnEDT(Object arg) { } } - private void statusChanged(Control.Status status, EnumSet features) { + private void statusChanged(Control.Status status, EnumSet features) { mCurrentStatus = status; mServerFeatures = features; From 960c3bb7ae45006a4633ba5af5c8d4ab602939c5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 19 Apr 2016 20:47:19 +0200 Subject: [PATCH 214/257] client: new class for getting file upload slots (XEP-0363) --- src/main/java/org/kontalk/client/Client.java | 39 +++++++-- .../org/kontalk/client/FeatureDiscovery.java | 14 ++-- .../kontalk/client/HTTPFileSlotRequester.java | 84 +++++++++++++++++++ src/main/java/org/kontalk/misc/Callback.java | 27 ++++++ .../org/kontalk/system/AttachmentManager.java | 14 ++++ .../java/org/kontalk/util/EncodingUtils.java | 14 ++++ .../java/org/kontalk/util/MediaUtils.java | 17 +--- 7 files changed, 182 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/kontalk/client/HTTPFileSlotRequester.java diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 5e57d174..47649534 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; +import java.util.EnumMap; import java.util.EnumSet; import java.util.List; import java.util.Optional; @@ -52,6 +53,7 @@ import org.kontalk.crypto.PersonalKey; import org.kontalk.misc.JID; import org.kontalk.model.message.OutMessage; +import org.kontalk.system.AttachmentManager; import org.kontalk.system.Control; import org.kontalk.system.RosterHandler; @@ -73,10 +75,11 @@ private enum Command {CONNECT, DISCONNECT}; private final Control mControl; private final KonMessageSender mMessageSender; - private final EnumSet mFeatures; + private final EnumMap mFeatures; private KonConnection mConn = null; private AvatarSendReceiver mAvatarSendReceiver = null; + private HTTPFileSlotRequester mSlotRequester = null; private Client(Control control, Path appDir) { mControl = control; @@ -87,7 +90,7 @@ private Client(Control control, Path appDir) { // enable Smack debugging (print raw XML packet) //SmackConfiguration.DEBUG = true; - mFeatures = EnumSet.noneOf(FeatureDiscovery.Feature.class); + mFeatures = new EnumMap<>(FeatureDiscovery.Feature.class); // setting caps cache File cacheDir = appDir.resolve(CAPS_CACHE_DIR).toFile(); @@ -207,7 +210,12 @@ private void connectAsync() { } mFeatures.clear(); - mFeatures.addAll(FeatureDiscovery.discover(mConn)); + mFeatures.putAll(FeatureDiscovery.discover(mConn)); + + mSlotRequester = mFeatures.containsKey(FeatureDiscovery.Feature.HTTP_FILE_UPLOAD) ? + new HTTPFileSlotRequester(mConn, + JID.bare(mFeatures.get(FeatureDiscovery.Feature.HTTP_FILE_UPLOAD))) : + null; // Caps, XEP-0115 // NOTE: caps manager is automatically used by Smack @@ -256,6 +264,7 @@ private void connectAsync() { this.newStatus(Control.Status.CONNECTED); } + public void disconnect() { synchronized (this) { if (mConn != null && mConn.isConnected()) { @@ -279,6 +288,12 @@ public Optional getOwnJID() { return Optional.of(JID.full(user)); } + public EnumSet getServerFeature() { + EnumSet e = EnumSet.noneOf(FeatureDiscovery.Feature.class); + e.addAll(mFeatures.keySet()); + return e; + } + public boolean sendMessage(OutMessage message, boolean sendChatState) { return mMessageSender.sendMessage(message, sendChatState); } @@ -445,7 +460,7 @@ public void publishAvatar(String id, byte[] data) { LOGGER.warning("no avatar sender"); return; } - if (mFeatures.contains(FeatureDiscovery.Feature.USER_AVATAR)) { + if (mFeatures.containsKey(FeatureDiscovery.Feature.USER_AVATAR)) { mAvatarSendReceiver.publish(id, data); } else { LOGGER.warning("not supported by server"); @@ -458,7 +473,7 @@ public boolean deleteAvatar() { return false; } - if (mFeatures.contains(FeatureDiscovery.Feature.USER_AVATAR)) { + if (mFeatures.containsKey(FeatureDiscovery.Feature.USER_AVATAR)) { return mAvatarSendReceiver.delete(); } else { LOGGER.warning("not supported by server"); @@ -466,13 +481,23 @@ public boolean deleteAvatar() { } } + /** Request upload slot (XEP-0636). Blocking */ + public AttachmentManager.Slot getUploadSlot(String name, long length, String mime) { + if (mSlotRequester == null) { + LOGGER.warning("no slot requester"); + return new AttachmentManager.Slot(); + } + + return mSlotRequester.getSlot(name, length, mime); + } + /* package internal*/ void newStatus(Control.Status status) { if (status != Control.Status.CONNECTED) mFeatures.clear(); - mControl.onStatusChange(status, mFeatures.clone()); + mControl.onStatusChange(status, this.getServerFeature()); } void newException(KonException konException) { @@ -480,7 +505,7 @@ void newException(KonException konException) { } String multiAddressHost() { - return mFeatures.contains(FeatureDiscovery.Feature.MULTI_ADDRESSING) + return mFeatures.containsKey(FeatureDiscovery.Feature.MULTI_ADDRESSING) && mConn != null ? mConn.getHost() : ""; } diff --git a/src/main/java/org/kontalk/client/FeatureDiscovery.java b/src/main/java/org/kontalk/client/FeatureDiscovery.java index 570e162f..4490c456 100644 --- a/src/main/java/org/kontalk/client/FeatureDiscovery.java +++ b/src/main/java/org/kontalk/client/FeatureDiscovery.java @@ -18,7 +18,7 @@ package org.kontalk.client; -import java.util.EnumSet; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,12 +61,12 @@ public enum Feature { } /** (server) service discovery, XEP-0030. */ - static EnumSet discover(XMPPConnection conn) { + static EnumMap discover(XMPPConnection conn) { // NOTE: smack automatically creates instances of SDM and CapsM and connects them ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(conn); // 1. get features from server - EnumSet features = discover(discoManager, conn.getServiceName()); + EnumMap features = discover(discoManager, conn.getServiceName()); DiscoverItems items = null; try { @@ -80,15 +80,15 @@ static EnumSet discover(XMPPConnection conn) { // 2. get features from server items for (DiscoverItems.Item item: items.getItems()) { - features.addAll(discover(discoManager, item.getEntityID())); + features.putAll(discover(discoManager, item.getEntityID())); } LOGGER.info("supported server features: "+features); return features; } - private static EnumSet discover(ServiceDiscoveryManager dm, String entity) { - EnumSet features = EnumSet.noneOf(Feature.class); + private static EnumMap discover(ServiceDiscoveryManager dm, String entity) { + EnumMap features = new EnumMap<>(FeatureDiscovery.Feature.class); DiscoverInfo info; try { // blocking @@ -110,7 +110,7 @@ private static EnumSet discover(ServiceDiscoveryManager dm, String enti for (DiscoverInfo.Feature feature: info.getFeatures()) { String var = feature.getVar(); if (FEATURE_MAP.containsKey(var)) { - features.add(FEATURE_MAP.get(var)); + features.put(FEATURE_MAP.get(var), entity); } } diff --git a/src/main/java/org/kontalk/client/HTTPFileSlotRequester.java b/src/main/java/org/kontalk/client/HTTPFileSlotRequester.java new file mode 100644 index 00000000..9b1ccf47 --- /dev/null +++ b/src/main/java/org/kontalk/client/HTTPFileSlotRequester.java @@ -0,0 +1,84 @@ +/* + * Kontalk Java client + * Copyright (C) 2016 Kontalk Devteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.client; + +import java.util.logging.Logger; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.provider.ProviderManager; +import org.kontalk.misc.Callback; +import org.kontalk.misc.JID; +import org.kontalk.system.AttachmentManager; +import org.kontalk.util.EncodingUtils; + +/** + * + * @author Alexander Bikadorov {@literal } + */ +final class HTTPFileSlotRequester { + private static final Logger LOGGER = Logger.getLogger(HTTPFileSlotRequester.class.getName()); + + static { + ProviderManager.addIQProvider( + HTTPFileUpload.Slot.ELEMENT_NAME, + HTTPFileUpload.NAMESPACE, + new HTTPFileUpload.Slot.Provider()); + } + + private final KonConnection mConn; + private final JID mService; + + public HTTPFileSlotRequester(KonConnection conn, JID service) { + mConn = conn; + mService = service; + } + + private HTTPFileUpload.Slot mSlotPacket; + synchronized AttachmentManager.Slot getSlot(String filename, long size, String mime) { + HTTPFileUpload.Request request = new HTTPFileUpload.Request(filename, size, mime); + request.setTo(mService.string()); + + final Callback.Synchronizer syncer = new Callback.Synchronizer(); + mSlotPacket = null; + mConn.sendWithCallback(request, new StanzaListener() { + @Override + public void processPacket(Stanza packet) + throws SmackException.NotConnectedException { + LOGGER.config("response: "+packet); + + if (!(packet instanceof HTTPFileUpload.Slot)) { + LOGGER.warning("response not a slot packet: "+packet); + syncer.sync(); + return; + } + mSlotPacket = (HTTPFileUpload.Slot) packet; + syncer.sync(); + } + }); + + syncer.waitForSync(); + + return mSlotPacket != null ? + new AttachmentManager.Slot( + EncodingUtils.toURI(mSlotPacket.getPutUrl()), + EncodingUtils.toURI(mSlotPacket.getGetUrl())) : + new AttachmentManager.Slot(); + } +} diff --git a/src/main/java/org/kontalk/misc/Callback.java b/src/main/java/org/kontalk/misc/Callback.java index b2c4b9fd..dfdaccf0 100644 --- a/src/main/java/org/kontalk/misc/Callback.java +++ b/src/main/java/org/kontalk/misc/Callback.java @@ -19,12 +19,18 @@ package org.kontalk.misc; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; /** * * @author Alexander Bikadorov {@literal } */ public final class Callback { + private static final Logger LOGGER = Logger.getLogger(Callback.class.getName()); + public final V value; public final Optional exception; @@ -46,4 +52,25 @@ public Callback(Exception ex) { public interface Handler { void handle(Callback callback); } + + public static class Synchronizer { + private final CountDownLatch mLatch = new CountDownLatch(1); + + public void sync() { + mLatch.countDown(); + } + + public boolean waitForSync() { + boolean succ = false; + try { + succ = mLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + LOGGER.log(Level.WARNING, "interrupted", ex); + } + if (!succ) { + LOGGER.warning("await failed, timeout reached"); + } + return succ; + } + } } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index edec8811..c1d0fa9f 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -411,4 +411,18 @@ static Attachment attachmentOrNull(Path path) { } return new Attachment(path, mimeType); } + + public static class Slot { + final URI uploadURL; + final URI downloadURL; + + public Slot() { + this(URI.create(""), URI.create("")); + } + + public Slot(URI uploadURI, URI downloadURL) { + this.uploadURL = uploadURI; + this.downloadURL = downloadURL; + } + } } diff --git a/src/main/java/org/kontalk/util/EncodingUtils.java b/src/main/java/org/kontalk/util/EncodingUtils.java index 61887c88..d3954886 100644 --- a/src/main/java/org/kontalk/util/EncodingUtils.java +++ b/src/main/java/org/kontalk/util/EncodingUtils.java @@ -18,13 +18,18 @@ package org.kontalk.util; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Base64; import java.util.EnumSet; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; import org.json.simple.JSONObject; public final class EncodingUtils { + private static final Logger LOGGER = Logger.getLogger(EncodingUtils.class.getName()); public static final String EOL = System.getProperty("line.separator"); @@ -80,4 +85,13 @@ public static byte[] base64ToBytes(String base64) { public static String bytesToBase64(byte[] bytes) { return Base64.getEncoder().encodeToString(bytes); } + + public static URI toURI(String str) { + try { + return new URI(str); + } catch (URISyntaxException ex) { + LOGGER.log(Level.WARNING, "invalid URI", ex); + } + return URI.create(""); + } } \ No newline at end of file diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 2a9b64ab..24bb7760 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -28,8 +28,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; @@ -37,6 +35,7 @@ import org.apache.tika.mime.MimeType; import org.apache.tika.mime.MimeTypeException; import org.apache.tika.mime.MimeTypes; +import org.kontalk.misc.Callback; /** * @@ -175,7 +174,7 @@ public static BufferedImage scale(Image image, int width, int height) { } private static BufferedImage toBufferedImage(Image image) { - final CountDownLatch latch = new CountDownLatch(1); + final Callback.Synchronizer syncer = new Callback.Synchronizer(); ImageObserver observer = new ImageObserver() { @Override @@ -186,21 +185,13 @@ public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, in } // scaling done, continue with calling thread - latch.countDown(); + syncer.sync(); return false; } }; if (image.getWidth(observer) == -1) { - boolean succ = false; - try { - succ = latch.await(5, TimeUnit.SECONDS); - } catch (InterruptedException ex) { - LOGGER.log(Level.WARNING, "interrupted", ex); - } - if (!succ) { - LOGGER.warning("await failed"); - } + syncer.waitForSync(); } // convert to buffered image, source: https://stackoverflow.com/a/13605411 From 74837295329546682ec3325e3b15aa2baa44ca80 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 16:27:17 +0200 Subject: [PATCH 215/257] utils: wrapped random string function --- src/main/java/org/kontalk/client/HTTPFileClient.java | 5 ++--- src/main/java/org/kontalk/model/Account.java | 4 ++-- src/main/java/org/kontalk/model/message/OutMessage.java | 4 ++-- src/main/java/org/kontalk/system/GroupControl.java | 4 ++-- src/main/java/org/kontalk/util/EncodingUtils.java | 5 +++++ 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index 9d017880..eca59628 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -55,6 +55,7 @@ import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.kontalk.misc.KonException; +import org.kontalk.util.EncodingUtils; import org.kontalk.util.MediaUtils; import org.kontalk.util.TrustUtils; @@ -160,9 +161,7 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx // fallback String type = StringUtils.defaultString(entity.getContentType().getValue()); String ext = MediaUtils.extensionForMIME(type); - filename = "att_" + - org.jivesoftware.smack.util.StringUtils.randomString(4) + - "." + ext; + filename = "att_" + EncodingUtils.randomString(4) + "." + ext; } // get file size diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index a8ab0d53..6c992df6 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -34,13 +34,13 @@ import org.apache.commons.io.IOUtils; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPException; -import org.jivesoftware.smack.util.StringUtils; import org.kontalk.misc.KonException; import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; import org.kontalk.misc.JID; import org.kontalk.persistence.Config; +import org.kontalk.util.EncodingUtils; /** * The user account. @@ -141,7 +141,7 @@ private void writePrivateKey(byte[] privateKeyData, // new password boolean unset = newPassword.length == 0; if (unset) - newPassword = StringUtils.randomString(40).toCharArray(); + newPassword = EncodingUtils.randomString(40).toCharArray(); // write new try { diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index eaa66b1e..12d7dd96 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -28,9 +28,9 @@ import java.util.Optional; import java.util.Set; import java.util.logging.Logger; -import org.jivesoftware.smack.util.StringUtils; import org.kontalk.crypto.Coder; import org.kontalk.model.Contact; +import org.kontalk.util.EncodingUtils; /** * Model for an XMPP message from the user to a contact. @@ -45,7 +45,7 @@ public OutMessage(Chat chat, List contacts, MessageContent content, boolean encrypted) { super( chat, - "Kon_" + StringUtils.randomString(8), + "Kon_" + EncodingUtils.randomString(8), content, Optional.empty(), Status.PENDING, diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 66fba13e..26882635 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -33,6 +33,7 @@ import org.kontalk.model.chat.Member; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.util.EncodingUtils; /** * Control logic for group chat management. @@ -229,7 +230,6 @@ Optional getGroupChat(MessageContent content, Contact sender) { } static KonGroupData newKonGroupData(JID myJID) { - return new KonGroupData(myJID, - org.jivesoftware.smack.util.StringUtils.randomString(8)); + return new KonGroupData(myJID, EncodingUtils.randomString(8)); } } diff --git a/src/main/java/org/kontalk/util/EncodingUtils.java b/src/main/java/org/kontalk/util/EncodingUtils.java index d3954886..2f018587 100644 --- a/src/main/java/org/kontalk/util/EncodingUtils.java +++ b/src/main/java/org/kontalk/util/EncodingUtils.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; import org.json.simple.JSONObject; @@ -94,4 +95,8 @@ public static URI toURI(String str) { } return URI.create(""); } + + public static String randomString(int length) { + return RandomStringUtils.randomAlphanumeric(length); + } } \ No newline at end of file From dc819d217e6b5e8a08cf53eebee4da789b0644f7 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 16:38:47 +0200 Subject: [PATCH 216/257] client: support in HTTP file client for new upload service (with XEP-0363) --- .../org/kontalk/client/HTTPFileClient.java | 63 ++++++++++++------- .../org/kontalk/system/AttachmentManager.java | 2 +- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index eca59628..90972cc1 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyManagementException; @@ -39,6 +40,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.output.CountingOutputStream; import org.apache.commons.lang.StringUtils; import org.apache.http.Header; @@ -46,8 +48,10 @@ import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -104,12 +108,10 @@ public void abort() { } /** - * Downloads to a directory represented by a {@link File} object, - * determining the file name from the Content-Disposition header. + * Download file to directory. * @param url URL of file * @param base base directory in which the download is saved - * @return the absolute path of the downloaded file, empty if the file could - * not be downloaded + * @return absolute path of downloaded file, empty if download failed */ public Path download(URI url, Path base, ProgressListener listener) throws KonException { if (mHTTPClient == null) { @@ -144,12 +146,10 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx throw new KonException(KonException.Error.DOWNLOAD_RESPONSE); } - // get filename + // try getting filename from header String filename = ""; Header dispHeader = response.getFirstHeader("Content-Disposition"); - if (dispHeader == null) { - LOGGER.warning("no content header"); - } else { + if (dispHeader != null) { filename = parseContentDisposition(dispHeader.getValue()); // never trust incoming data filename = Paths.get(filename).getFileName().toString(); @@ -157,6 +157,7 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx LOGGER.warning("can't parse filename in content: "+dispHeader.getValue()); } } + // NOTE: could try getting the extension (and filename) from URL, security? if (filename.isEmpty()) { // fallback String type = StringUtils.defaultString(entity.getContentType().getValue()); @@ -179,12 +180,18 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx final long fileSize = s; mCurrentListener.updateProgress(s < 0 ? -2 : 0); - File destination = new File(base.toString(), filename); - if (destination.exists()) { - LOGGER.warning("file already exists: "+destination.getAbsolutePath()); - return Paths.get(""); + Path destination = Paths.get(base.toString(), filename); + if (Files.exists(destination)) { + destination = Paths.get(base.toString(), + FilenameUtils.getBaseName(filename) + + "_" + EncodingUtils.randomString(4) + + "." + FilenameUtils.getExtension(filename)); + if (Files.exists(destination)) { + LOGGER.warning("not possible"); + return Paths.get(""); + } } - try (FileOutputStream out = new FileOutputStream(destination)){ + try (FileOutputStream out = new FileOutputStream(destination.toFile())){ CountingOutputStream cOut = new CountingOutputStream(out) { @Override protected synchronized void afterWrite(int n) { @@ -209,7 +216,7 @@ protected synchronized void afterWrite(int n) { mCurrentRequest = null; mCurrentListener = null; - return Paths.get(destination.getAbsolutePath()); + return destination.toAbsolutePath(); } finally { try { response.close(); @@ -221,14 +228,23 @@ protected synchronized void afterWrite(int n) { } /** - * Upload a file. - * @param file file to download - * @param url the upload URL pointing to the upload service - * @param mime mime-type of file - * @param encrypted is the file encrypted? + * Upload file using a PUT request. * @return the URL the file can be downloaded with. */ - public URI upload(File file, URI url, String mime, boolean encrypted) throws KonException { + public void upload(File file, URI uploadURL, String mime, boolean encrypted) throws KonException { + this.upload(file, uploadURL, mime, encrypted, false); + } + + /** + * Upload file using a POST request and parse download URL from response. + * @return the URL the uploaded file can be downloaded with. + */ + public URI uploadLegacy(File file, URI url, String mime, boolean encrypted) throws KonException { + return this.upload(file, url, mime, encrypted, true); + } + + private URI upload(File file, URI url, String mime, boolean encrypted, boolean legacy) + throws KonException { if (mHTTPClient == null) { mHTTPClient = httpClientOrNull(mPrivateKey, mCertificate, mValidateCertificate); if (mHTTPClient == null) @@ -236,7 +252,9 @@ public URI upload(File file, URI url, String mime, boolean encrypted) throws Kon } // request type - HttpPost req = new HttpPost(url); + HttpEntityEnclosingRequestBase req = legacy ? + new HttpPost(url) : + new HttpPut(url); req.setHeader("Content-Type", mime); if (encrypted) req.addHeader(HEADER_MESSAGE_FLAGS, "encrypted"); @@ -264,6 +282,9 @@ public URI upload(File file, URI url, String mime, boolean encrypted) throws Kon throw new KonException(KonException.Error.UPLOAD_RESPONSE); } + if (!legacy) + return null; + HttpEntity entity = response.getEntity(); if (entity == null) { LOGGER.warning("no upload response entity"); diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index c1d0fa9f..77d5bcd9 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -203,7 +203,7 @@ private void uploadAsync(OutMessage message) { URI url; try { - url = client.upload(file, UPLOAD_URI, mime, encrypt); + url = client.uploadLegacy(file, UPLOAD_URI, mime, encrypt); } catch (KonException ex) { LOGGER.warning("upload failed, attachment: "+attachment); message.setStatus(KonMessage.Status.ERROR); From 5ce8cc36c4591b391cad582aa8c1b69cbb8b99d5 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 16:54:44 +0200 Subject: [PATCH 217/257] control: support in attachment manager for new upload service (with XEP-0363) --- .../org/kontalk/system/AttachmentManager.java | 64 +++++++++++++------ src/main/java/org/kontalk/system/Control.java | 2 +- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 77d5bcd9..19017614 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -34,6 +34,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; +import org.kontalk.client.Client; +import org.kontalk.client.FeatureDiscovery; import org.kontalk.client.HTTPFileClient; import org.kontalk.crypto.Coder; import org.kontalk.crypto.Coder.Encryption; @@ -65,7 +67,7 @@ public class AttachmentManager implements Runnable { public static final String THUMBNAIL_MIME = "image/jpeg"; public static final int MAX_ATT_SIZE = 10 * 1024 * 1024; - // server and Android client do not want other types + // server (?) and Android client do not want other types public static final List SUPPORTED_MIME_TYPES = Arrays.asList( "text/plain", "text/x-vcard", @@ -78,10 +80,11 @@ public class AttachmentManager implements Runnable { "audio/mpeg3", "audio/wav"); - // TODO get this from server - private static final URI UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); + private static final URI LEGACY_UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); + private static final String ENCRYPT_MIME = "application/octet-stream"; private final Control mControl; + private final Client mClient; private final LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); private final Path mAttachmentDir; @@ -108,8 +111,9 @@ public DownloadTask(InMessage message) { } } - private AttachmentManager(Control control, Path baseDir) { + private AttachmentManager(Control control, Client client, Path baseDir) { mControl = control; + mClient = client; mAttachmentDir = baseDir.resolve(ATT_DIRNAME); if (mAttachmentDir.toFile().mkdir()) LOGGER.info("created attachment directory"); @@ -119,8 +123,8 @@ private AttachmentManager(Control control, Path baseDir) { LOGGER.info("created preview directory"); } - static AttachmentManager create(Control control, Path appDir) { - AttachmentManager manager = new AttachmentManager(control, appDir); + static AttachmentManager create(Control control, Client client, Path appDir) { + AttachmentManager manager = new AttachmentManager(control, client, appDir); Thread thread = new Thread(manager, "Attachment Transfer"); thread.setDaemon(true); @@ -181,6 +185,10 @@ private void uploadAsync(OutMessage message) { } } + // use old file server or new upload service with XEP-0363? + boolean legacyUpload = !mClient.getServerFeature() + .contains(FeatureDiscovery.Feature.HTTP_FILE_UPLOAD); + // if text will be encrypted, always encrypt attachment too boolean encrypt = message.getCoderStatus().getEncryption() == Encryption.DECRYPTED; if (encrypt) { @@ -193,36 +201,52 @@ private void uploadAsync(OutMessage message) { if (encryptFile == null) return; file = encryptFile; - // mime (and length) actually changed, but the server can't handle the truth - //mime = "application/octet-stream"; + // NOTE: legacy upload service doesn't accept encrypted mime + if (!legacyUpload) { + mime = ENCRYPT_MIME; + } } + long length = file.length(); + HTTPFileClient client = this.clientOrNull(); if (client == null) return; - URI url; - try { - url = client.uploadLegacy(file, UPLOAD_URI, mime, encrypt); - } catch (KonException ex) { - LOGGER.warning("upload failed, attachment: "+attachment); - message.setStatus(KonMessage.Status.ERROR); - mControl.onException(ex); - return; + URI downloadURL; + if (legacyUpload) { + try { + downloadURL = client.uploadLegacy(file, LEGACY_UPLOAD_URI, mime, encrypt); + } catch (KonException ex) { + LOGGER.warning("legacy upload failed, attachment: "+attachment); + message.setStatus(KonMessage.Status.ERROR); + mControl.onException(ex); + return; + } + } else { + Slot uploadSlot = mClient.getUploadSlot(file.getName(), length, mime); + try { + client.upload(file, uploadSlot.uploadURL, mime, encrypt); + } catch (KonException ex) { + LOGGER.warning("upload failed, attachment: "+attachment); + message.setStatus(KonMessage.Status.ERROR); + mControl.onException(ex); + return; + } + downloadURL = uploadSlot.downloadURL; } - long length = file.length(); if (!file.equals(original)) file.delete(); - if (url.toString().isEmpty()) { + if (downloadURL.toString().isEmpty()) { LOGGER.warning("url empty: "+attachment); return; } - message.setUpload(url, mime, length); + message.setUpload(downloadURL, mime, length); - LOGGER.info("upload successful, URL="+url); + LOGGER.info("upload successful, URL="+downloadURL); // make sure not to loop if (attachment.hasURL()) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index ac7afe99..fc79292c 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -114,7 +114,7 @@ public Control(Path appDir) throws KonException { mClient = Client.create(this, appDir); mChatStateManager = new ChatStateManager(mClient); - mAttachmentManager = AttachmentManager.create(this, appDir); + mAttachmentManager = AttachmentManager.create(this, mClient, appDir); mRosterHandler = new RosterHandler(this, mClient, mModel); mAvatarHandler = new AvatarHandler(mClient, mModel); mGroupControl = new GroupControl(this, mModel); From 6d7331d77bc6daedeacfdd9ec17f3e7537391b57 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 17:04:35 +0200 Subject: [PATCH 218/257] view: outgoing messages with errors can be send again --- src/main/java/org/kontalk/system/Control.java | 6 ++++++ .../java/org/kontalk/view/MessageList.java | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index fc79292c..0291f772 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -790,6 +790,12 @@ public void sendAttachment(Chat chat, Path file){ this.sendNewMessage(chat, "", file); } + public void sendAgain(OutMessage outMessage) { + Control.this.sendMessage(outMessage); + } + + /* avatar */ + public void setUserAvatar(BufferedImage image) { Avatar.UserAvatar newAvatar = mModel.setUserAvatar(image); byte[] avatarData = newAvatar.imageData().orElse(null); diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index bd61d7fe..8f5df1bb 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -65,6 +65,7 @@ import org.kontalk.model.chat.Member; import org.kontalk.model.message.MessageContent.Attachment; import org.kontalk.model.message.MessageContent.GroupCommand; +import org.kontalk.model.message.OutMessage; import org.kontalk.model.message.Transmission; import org.kontalk.util.Tr; import org.kontalk.view.ChatView.Background; @@ -73,6 +74,7 @@ /** * View all messages of one chat in a left/right MIM style list. + * * @author Alexander Bikadorov {@literal } */ final class MessageList extends ListView { @@ -176,13 +178,14 @@ protected WebPopupMenu rightClickMenu(MessageItem item) { final KonMessage m = item.mValue; if (m instanceof InMessage) { + InMessage im = (InMessage) m; if (m.isEncrypted()) { WebMenuItem decryptMenuItem = new WebMenuItem(Tr.tr("Decrypt")); decryptMenuItem.setToolTipText(Tr.tr("Retry decrypting message")); decryptMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - mView.getControl().decryptAgain((InMessage) m); + mView.getControl().decryptAgain(im); } }); menu.add(decryptMenuItem); @@ -195,11 +198,23 @@ public void actionPerformed(ActionEvent event) { attMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - mView.getControl().downloadAgain((InMessage) m); + mView.getControl().downloadAgain(im); } }); menu.add(attMenuItem); } + } else if (m instanceof OutMessage) { + if (m.getStatus() == KonMessage.Status.ERROR) { + WebMenuItem sendMenuItem = new WebMenuItem(Tr.tr("Retry")); + sendMenuItem.setToolTipText(Tr.tr("Retry sending message")); + sendMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.getControl().sendAgain((OutMessage) m); + } + }); + menu.add(sendMenuItem); + } } WebMenuItem cItem = Utils.createCopyMenuItem( From f10d8e580c2b285c9ad3d71d16d5faef49770c1f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 20:24:06 +0200 Subject: [PATCH 219/257] client: use attachment upload URL as body text (if there is nothing else) --- .../java/org/kontalk/client/KonMessageSender.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 0603dec3..0f2fb9ff 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -73,7 +73,9 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { Chat chat = message.getChat(); - Message protoMessage = encrypted ? new Message() : rawMessage(content, chat, false); + Message protoMessage = encrypted ? + new Message() : + rawMessage(content, chat, false); protoMessage.setType(Message.Type.chat); protoMessage.setStanzaId(message.getXMPPID()); @@ -134,13 +136,18 @@ boolean sendMessage(OutMessage message, boolean sendChatState) { public static Message rawMessage(MessageContent content, Chat chat, boolean encrypted) { Message smackMessage = new Message(); - // text + MessageContent.Attachment att = content.getAttachment().orElse(null); + + // text body String text = content.getPlainText(); + if (text.isEmpty() && att != null) { + // use attachment URL as body + text = att.getURL().toString(); + } if (!text.isEmpty()) - smackMessage.setBody(content.getPlainText()); + smackMessage.setBody(text); // attachment - MessageContent.Attachment att = content.getAttachment().orElse(null); if (att != null) { OutOfBandData oobData = new OutOfBandData(att.getURL().toString(), att.getMimeType(), att.getLength(), encrypted); From cc377c21a33aa30129cc3a8b884c255d0e006892 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 20 Apr 2016 20:47:39 +0200 Subject: [PATCH 220/257] control: get mime attachment preview from file if not in OOB extension --- .../kontalk/model/message/MessageContent.java | 5 +- .../org/kontalk/model/message/OutMessage.java | 2 +- .../org/kontalk/system/AttachmentManager.java | 69 +++++++++++-------- src/main/java/org/kontalk/system/Control.java | 2 +- .../java/org/kontalk/view/MessageList.java | 7 +- 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index b49461fd..747c4e53 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -287,7 +287,8 @@ public Attachment(Path path, String mimeType) { public Attachment(URI url, String mimeType, long length, boolean encrypted) { this(url, Paths.get(""), mimeType, length, - encrypted ? CoderStatus.createEncrypted() : + encrypted ? + CoderStatus.createEncrypted() : CoderStatus.createInsecure() ); } @@ -311,7 +312,7 @@ public URI getURL() { return mURL; } - public void update(URI url, String mime, long length){ + void updateUploaded(URI url, String mime, long length){ mURL = url; mMimeType = mime; mLength = length; diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 12d7dd96..8a26a45b 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -116,7 +116,7 @@ public void setUpload(URI url, String mime, long length) { if (attachment == null) return; - attachment.update(url, mime, length); + attachment.updateUploaded(url, mime, length); this.save(); } diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 19017614..d39634d1 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -34,6 +34,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; import org.kontalk.client.Client; import org.kontalk.client.FeatureDiscovery; import org.kontalk.client.HTTPFileClient; @@ -80,6 +81,20 @@ public class AttachmentManager implements Runnable { "audio/mpeg3", "audio/wav"); + public static class Slot { + final URI uploadURL; + final URI downloadURL; + + public Slot() { + this(URI.create(""), URI.create("")); + } + + public Slot(URI uploadURI, URI downloadURL) { + this.uploadURL = uploadURI; + this.downloadURL = downloadURL; + } + } + private static final URI LEGACY_UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); private static final String ENCRYPT_MIME = "application/octet-stream"; @@ -158,7 +173,7 @@ private void uploadAsync(OutMessage message) { File file = original = attachment.getFilePath().toFile(); String mime = attachment.getMimeType(); - if(isImage(attachment.getMimeType())) { + if(isImage(mime)) { int maxImgSize = Config.getInstance().getInt(Config.NET_MAX_IMG_SIZE); if (maxImgSize > 0) { BufferedImage img = MediaUtils.readImage(file).orElse(null); @@ -300,7 +315,7 @@ public void updateProgress(int p) { this.createImagePreview(message); } - public void savePreview(InMessage message) { + void savePreview(InMessage message) { Preview preview = message.getContent().getPreview().orElse(null); if (preview == null) { LOGGER.warning("no preview in message: "+message); @@ -322,7 +337,12 @@ boolean createImagePreview(KonMessage message) { } Path path = absoluteFilePath(att); - if (!isImage(att.getMimeType())) + String mime = att.getMimeType(); + if (mime.isEmpty()) + // guess from file + mime = mimeForFile(path); + + if (!isImage(mime)) return false; BufferedImage image = MediaUtils.readImage(path); @@ -394,6 +414,10 @@ private HTTPFileClient clientOrNull(){ Config.getInstance().getBoolean(Config.SERV_CERT_VALIDATION)); } + private static boolean isImage(String mimeType) { + return mimeType.startsWith("image"); + } + @Override public void run() { while (true) { @@ -413,40 +437,31 @@ public void run() { } } - public static boolean isImage(String mimeType) { - return mimeType.startsWith("image"); - } - /** * Create a new attachment for a given file denoted by its path. */ - static Attachment attachmentOrNull(Path path) { - File file = path.toFile(); - if (!file.isFile() || !file.canRead()) { - LOGGER.warning("invalid attachment file: "+path); + static Attachment createAttachmentOrNull(Path path) { + if (!Files.isReadable(path)) { + LOGGER.warning("file not readable: "+path); return null; } - String mimeType; - try { - mimeType = Files.probeContentType(path); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't get attachment mime type", ex); + + String mimeType = mimeForFile(path); + if (mimeType.isEmpty()) { + LOGGER.warning("no mime type for file: "+path); return null; } + return new Attachment(path, mimeType); } - public static class Slot { - final URI uploadURL; - final URI downloadURL; - - public Slot() { - this(URI.create(""), URI.create("")); - } - - public Slot(URI uploadURI, URI downloadURL) { - this.uploadURL = uploadURI; - this.downloadURL = downloadURL; + private static String mimeForFile(Path path) { + String mime = null; + try { + mime = Files.probeContentType(path); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't probe type", ex); } + return StringUtils.defaultString(mime); } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 0291f772..794200a9 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -819,7 +819,7 @@ public void unsetUserAvatar(){ private void sendNewMessage(Chat chat, String text, Path file) { Attachment attachment = null; if (!file.toString().isEmpty()) { - attachment = AttachmentManager.attachmentOrNull(file); + attachment = AttachmentManager.createAttachmentOrNull(file); if (attachment == null) return; } diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index 8f5df1bb..c32cab85 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -572,14 +572,13 @@ private void updateAttachment() { } // image thumbnail preview - Path imagePath = mView.getControl().getImagePath(mValue).orElse(null); - Path image = imagePath != null ? imagePath : Paths.get(""); - mAttPanel.setImage(image); + Path imagePath = mView.getControl().getImagePath(mValue).orElse(Paths.get("")); + mAttPanel.setImage(imagePath); // link to the file Path linkPath = mView.getControl().getFilePath(att); if (!linkPath.toString().isEmpty()) { - mAttPanel.setLink(image.toString().isEmpty() ? + mAttPanel.setLink(imagePath.toString().isEmpty() ? linkPath.getFileName().toString() : "", linkPath); From 36e469b08c69725b06b01dd32938b089d6d246bf Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 21 Apr 2016 19:47:54 +0200 Subject: [PATCH 221/257] moved message content parsing to utils --- .../kontalk/client/KonMessageListener.java | 76 +------------------ .../java/org/kontalk/crypto/Decryptor.java | 3 +- .../java/org/kontalk/util/ClientUtils.java | 66 +++++++++++++++- 3 files changed, 68 insertions(+), 77 deletions(-) diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 95cbe65f..72bbb625 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -18,11 +18,8 @@ package org.kontalk.client; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Date; import java.util.Optional; -import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; @@ -43,15 +40,10 @@ import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.kontalk.misc.JID; -import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.message.MessageContent; -import org.kontalk.model.message.MessageContent.Attachment; -import org.kontalk.model.message.MessageContent.GroupCommand; -import org.kontalk.model.message.MessageContent.Preview; import org.kontalk.system.Control; import org.kontalk.util.ClientUtils; import org.kontalk.util.ClientUtils.MessageIDs; -import org.kontalk.util.EncodingUtils; /** * Listen and handle all incoming XMPP message packets. @@ -170,7 +162,7 @@ private void processChatMessage(Message m) { // must be an incoming message // get content/text from body and/or encryption/url extension - MessageContent content = parseMessageContent(m); + MessageContent content = ClientUtils.parseMessageContent(m); // make sure not to save a message without content if (content.isEmpty()) { @@ -217,70 +209,4 @@ private void processHeadlineMessage(Message m) { LOGGER.warning("unhandled"); } - - public static MessageContent parseMessageContent(Message m) { - // default body - String plainText = StringUtils.defaultString(m.getBody()); - - // encryption extension (RFC 3923), decrypted later - String encrypted = ""; - ExtensionElement encryptionExt = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); - if (encryptionExt instanceof E2EEncryption) { - if (m.getBody() != null) - LOGGER.config("message contains encryption and body (ignoring body): "+m.getBody()); - E2EEncryption encryption = (E2EEncryption) encryptionExt; - encrypted = EncodingUtils.bytesToBase64(encryption.getData()); - } - - // Bits of Binary: preview for file attachment - Preview preview = null; - ExtensionElement bobExt = m.getExtension(BitsOfBinary.ELEMENT_NAME, BitsOfBinary.NAMESPACE); - if (bobExt instanceof BitsOfBinary) { - BitsOfBinary bob = (BitsOfBinary) bobExt; - String mime = StringUtils.defaultString(bob.getType()); - byte[] bits = bob.getContents(); - if (bits == null) - bits = new byte[0]; - if (mime.isEmpty() || bits.length <= 0) - LOGGER.warning("invalid BOB data: "+bob.toXML()); - else - preview = new Preview(bits, mime); - } - - // Out of Band Data: a URI to a file - Attachment attachment = null; - ExtensionElement oobExt = m.getExtension(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE); - if (oobExt instanceof OutOfBandData) { - OutOfBandData oobData = (OutOfBandData) oobExt; - URI url; - try { - url = new URI(oobData.getUrl()); - } catch (URISyntaxException ex) { - LOGGER.log(Level.WARNING, "can't parse URL", ex); - url = URI.create(""); - } - attachment = new MessageContent.Attachment(url, - oobData.getMime() != null ? oobData.getMime() : "", - oobData.getLength(), - oobData.isEncrypted()); - } - - // group command - KonGroupData gid = null; - GroupCommand groupCommand = null; - ExtensionElement groupExt = m.getExtension(GroupExtension.ELEMENT_NAME, - GroupExtension.NAMESPACE); - if (groupExt instanceof GroupExtension) { - GroupExtension group = (GroupExtension) groupExt; - gid = new KonGroupData(JID.bare(group.getOwner()), group.getID()); - groupCommand = ClientUtils.groupExtensionToGroupCommand( - group.getType(), group.getMembers(), group.getSubject()).orElse(null); - } - - return new MessageContent.Builder(plainText, encrypted) - .attachment(attachment) - .preview(preview) - .groupData(gid) - .groupCommand(groupCommand).build(); - } } diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 6bcbe1ad..2804420c 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -57,6 +57,7 @@ import org.kontalk.model.message.DecryptMessage; import org.kontalk.model.message.InMessage; import org.kontalk.util.CPIMMessage; +import org.kontalk.util.ClientUtils; import org.kontalk.util.XMPPUtils; import org.xmlpull.v1.XmlPullParserException; @@ -391,7 +392,7 @@ private static MessageContent parseCPIMOrNull(DecryptMessage message, String cpi return null; } LOGGER.config("decrypted XML: "+m.toXML()); - decryptedContent = KonMessageListener.parseMessageContent(m); + decryptedContent = ClientUtils.parseMessageContent(m); } else { // text/plain MIME type for simple text messages decryptedContent = MessageContent.plainText(content); diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 4fb4dacc..aa12d417 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -18,27 +18,36 @@ package org.kontalk.util; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.StringUtils; +import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; +import org.kontalk.client.BitsOfBinary; +import org.kontalk.client.E2EEncryption; import org.kontalk.client.GroupExtension; import org.kontalk.client.GroupExtension.Type; import org.kontalk.client.GroupExtension.Member; +import org.kontalk.client.OutOfBandData; import org.kontalk.model.Contact; import org.kontalk.misc.JID; import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; +import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.MessageContent.GroupCommand; import org.kontalk.model.message.MessageContent.GroupCommand.OP; /** - * + * Static utilities as interface between client and control. + * * @author Alexander Bikadorov {@literal } */ public final class ClientUtils { @@ -85,6 +94,61 @@ public String toString() { } } + public static MessageContent parseMessageContent(Message m) { + // default body + String plainText = StringUtils.defaultString(m.getBody()); + // encryption extension (RFC 3923), decrypted later + String encrypted = ""; + ExtensionElement encryptionExt = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); + if (encryptionExt instanceof E2EEncryption) { + if (m.getBody() != null) { + LOGGER.config("message contains encryption and body (ignoring body): " + m.getBody()); + } + E2EEncryption encryption = (E2EEncryption) encryptionExt; + encrypted = EncodingUtils.bytesToBase64(encryption.getData()); + } + // Bits of Binary: preview for file attachment + MessageContent.Preview preview = null; + ExtensionElement bobExt = m.getExtension(BitsOfBinary.ELEMENT_NAME, BitsOfBinary.NAMESPACE); + if (bobExt instanceof BitsOfBinary) { + BitsOfBinary bob = (BitsOfBinary) bobExt; + String mime = StringUtils.defaultString(bob.getType()); + byte[] bits = bob.getContents(); + if (bits == null) { + bits = new byte[0]; + } + if (mime.isEmpty() || bits.length <= 0) { + LOGGER.warning("invalid BOB data: " + bob.toXML()); + } else { + preview = new MessageContent.Preview(bits, mime); + } + } + // Out of Band Data: a URI to a file + MessageContent.Attachment attachment = null; + ExtensionElement oobExt = m.getExtension(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE); + if (oobExt instanceof OutOfBandData) { + OutOfBandData oobData = (OutOfBandData) oobExt; + URI url; + try { + url = new URI(oobData.getUrl()); + } catch (URISyntaxException ex) { + LOGGER.log(Level.WARNING, "can't parse URL", ex); + url = URI.create(""); + } + attachment = new MessageContent.Attachment(url, oobData.getMime() != null ? oobData.getMime() : "", oobData.getLength(), oobData.isEncrypted()); + } + // group command + KonGroupData gid = null; + GroupCommand groupCommand = null; + ExtensionElement groupExt = m.getExtension(GroupExtension.ELEMENT_NAME, GroupExtension.NAMESPACE); + if (groupExt instanceof GroupExtension) { + GroupExtension group = (GroupExtension) groupExt; + gid = new KonGroupData(JID.bare(group.getOwner()), group.getID()); + groupCommand = ClientUtils.groupExtensionToGroupCommand(group.getType(), group.getMembers(), group.getSubject()).orElse(null); + } + return new MessageContent.Builder(plainText, encrypted).attachment(attachment).preview(preview).groupData(gid).groupCommand(groupCommand).build(); + } + /* Internal to external */ public static GroupExtension groupCommandToGroupExtension(KonGroupChat chat, GroupCommand groupCommand) { From f514a59189567117622545467699ca4670d8442e Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 21 Apr 2016 20:01:53 +0200 Subject: [PATCH 222/257] utils: ignore attachment link in message body if present in OOB extension --- .../java/org/kontalk/util/ClientUtils.java | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index aa12d417..f5040680 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -42,12 +42,14 @@ import org.kontalk.model.chat.GroupChat.KonGroupChat; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.model.message.MessageContent; +import org.kontalk.model.message.MessageContent.Attachment; import org.kontalk.model.message.MessageContent.GroupCommand; import org.kontalk.model.message.MessageContent.GroupCommand.OP; +import org.kontalk.model.message.MessageContent.Preview; /** * Static utilities as interface between client and control. - * + * * @author Alexander Bikadorov {@literal } */ public final class ClientUtils { @@ -97,34 +99,34 @@ public String toString() { public static MessageContent parseMessageContent(Message m) { // default body String plainText = StringUtils.defaultString(m.getBody()); + // encryption extension (RFC 3923), decrypted later String encrypted = ""; ExtensionElement encryptionExt = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); if (encryptionExt instanceof E2EEncryption) { - if (m.getBody() != null) { - LOGGER.config("message contains encryption and body (ignoring body): " + m.getBody()); - } + if (m.getBody() != null) + LOGGER.config("message contains encryption and body (ignoring body): "+m.getBody()); E2EEncryption encryption = (E2EEncryption) encryptionExt; encrypted = EncodingUtils.bytesToBase64(encryption.getData()); } + // Bits of Binary: preview for file attachment - MessageContent.Preview preview = null; + Preview preview = null; ExtensionElement bobExt = m.getExtension(BitsOfBinary.ELEMENT_NAME, BitsOfBinary.NAMESPACE); if (bobExt instanceof BitsOfBinary) { BitsOfBinary bob = (BitsOfBinary) bobExt; String mime = StringUtils.defaultString(bob.getType()); byte[] bits = bob.getContents(); - if (bits == null) { + if (bits == null) bits = new byte[0]; - } - if (mime.isEmpty() || bits.length <= 0) { - LOGGER.warning("invalid BOB data: " + bob.toXML()); - } else { - preview = new MessageContent.Preview(bits, mime); - } + if (mime.isEmpty() || bits.length <= 0) + LOGGER.warning("invalid BOB data: "+bob.toXML()); + else + preview = new Preview(bits, mime); } + // Out of Band Data: a URI to a file - MessageContent.Attachment attachment = null; + Attachment attachment = null; ExtensionElement oobExt = m.getExtension(OutOfBandData.ELEMENT_NAME, OutOfBandData.NAMESPACE); if (oobExt instanceof OutOfBandData) { OutOfBandData oobData = (OutOfBandData) oobExt; @@ -135,18 +137,34 @@ public static MessageContent parseMessageContent(Message m) { LOGGER.log(Level.WARNING, "can't parse URL", ex); url = URI.create(""); } - attachment = new MessageContent.Attachment(url, oobData.getMime() != null ? oobData.getMime() : "", oobData.getLength(), oobData.isEncrypted()); + attachment = new MessageContent.Attachment(url, + oobData.getMime() != null ? oobData.getMime() : "", + oobData.getLength(), + oobData.isEncrypted()); + + // body text is maybe URI, for clients that dont understand OOB, + // but we do, don't save it twice + if (plainText.equals(url.toString())); + plainText = ""; } + // group command KonGroupData gid = null; GroupCommand groupCommand = null; - ExtensionElement groupExt = m.getExtension(GroupExtension.ELEMENT_NAME, GroupExtension.NAMESPACE); + ExtensionElement groupExt = m.getExtension(GroupExtension.ELEMENT_NAME, + GroupExtension.NAMESPACE); if (groupExt instanceof GroupExtension) { GroupExtension group = (GroupExtension) groupExt; gid = new KonGroupData(JID.bare(group.getOwner()), group.getID()); - groupCommand = ClientUtils.groupExtensionToGroupCommand(group.getType(), group.getMembers(), group.getSubject()).orElse(null); + groupCommand = ClientUtils.groupExtensionToGroupCommand( + group.getType(), group.getMembers(), group.getSubject()).orElse(null); } - return new MessageContent.Builder(plainText, encrypted).attachment(attachment).preview(preview).groupData(gid).groupCommand(groupCommand).build(); + + return new MessageContent.Builder(plainText, encrypted) + .attachment(attachment) + .preview(preview) + .groupData(gid) + .groupCommand(groupCommand).build(); } /* Internal to external */ From 4af19b39c3c2dce3c10a25a9c91805281e7e989a Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 21 Apr 2016 20:05:50 +0200 Subject: [PATCH 223/257] less INFO logging for attachment transfer --- .../java/org/kontalk/client/HTTPFileClient.java | 4 +++- .../java/org/kontalk/system/AttachmentManager.java | 14 +++++++------- src/main/java/org/kontalk/system/Control.java | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index 90972cc1..6e2045d8 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -120,7 +120,7 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx throw new KonException(KonException.Error.DOWNLOAD_CREATE); } - LOGGER.info("from URL=" + url+ "..."); + LOGGER.config("from URL=" + url+ " ..."); mCurrentRequest = new HttpGet(url); mCurrentListener = listener; @@ -259,6 +259,8 @@ private URI upload(File file, URI url, String mime, boolean encrypted, boolean l if (encrypted) req.addHeader(HEADER_MESSAGE_FLAGS, "encrypted"); + LOGGER.config("to URL=" + url+ " ..."); + // execute request CloseableHttpResponse response; try(FileInputStream in = new FileInputStream(file)) { diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index d39634d1..9424ca36 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -312,7 +312,7 @@ public void updateProgress(int p) { // create preview if not in message if (!message.getContent().getPreview().isPresent()) - this.createImagePreview(message); + this.mayCreateImagePreview(message); } void savePreview(InMessage message) { @@ -329,7 +329,7 @@ void savePreview(InMessage message) { message.setPreviewFilename(filename); } - boolean createImagePreview(KonMessage message) { + boolean mayCreateImagePreview(KonMessage message) { Attachment att = message.getContent().getAttachment().orElse(null); if (att == null) { LOGGER.warning("no attachment in message: "+message); @@ -401,7 +401,7 @@ private void writePreview(Preview preview, String filename) { return; } - LOGGER.info("to file: "+newFile); + LOGGER.config("to file: "+newFile); } private HTTPFileClient clientOrNull(){ @@ -414,10 +414,6 @@ private HTTPFileClient clientOrNull(){ Config.getInstance().getBoolean(Config.SERV_CERT_VALIDATION)); } - private static boolean isImage(String mimeType) { - return mimeType.startsWith("image"); - } - @Override public void run() { while (true) { @@ -464,4 +460,8 @@ private static String mimeForFile(Path path) { } return StringUtils.defaultString(mime); } + + private static boolean isImage(String mimeType) { + return mimeType.startsWith("image"); + } } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 794200a9..1aec432b 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -412,7 +412,7 @@ boolean createAndSendMessage(Chat chat, MessageContent content) { return false; if (newMessage.getContent().getAttachment().isPresent()) - mAttachmentManager.createImagePreview(newMessage); + mAttachmentManager.mayCreateImagePreview(newMessage); return this.sendMessage(newMessage); } From af19b05f68f13b3fe24ddb68b0e88a9ae81e8f96 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 17:14:13 +0200 Subject: [PATCH 224/257] control: no MIME type restriction for file upload anymore --- .../org/kontalk/system/AttachmentManager.java | 15 --------------- src/main/java/org/kontalk/view/ChatView.java | 4 +--- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 9424ca36..b7a91414 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -27,8 +27,6 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; import java.util.Optional; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; @@ -68,19 +66,6 @@ public class AttachmentManager implements Runnable { public static final String THUMBNAIL_MIME = "image/jpeg"; public static final int MAX_ATT_SIZE = 10 * 1024 * 1024; - // server (?) and Android client do not want other types - public static final List SUPPORTED_MIME_TYPES = Arrays.asList( - "text/plain", - "text/x-vcard", - "text/vcard", - "image/gif", - "image/png", - "image/jpeg", - "image/jpg", - "audio/3gpp", - "audio/mpeg3", - "audio/wav"); - public static class Slot { final URI uploadURL; final URI downloadURL; diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 2a40dda1..27fb7ac2 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -221,9 +221,7 @@ public void actionPerformed(ActionEvent e) { Tr.tr("Supported files")) { @Override public boolean accept(File file) { - return file.length() <= AttachmentManager.MAX_ATT_SIZE && - AttachmentManager.SUPPORTED_MIME_TYPES.contains( - TIKA_INSTANCE.detect(file.getName())); + return file.length() <= AttachmentManager.MAX_ATT_SIZE; } }); // mAttField.setPreferredWidth(150); From 2de5b9815a44476c932e2381f6b14464b8d38f8b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 18:26:52 +0200 Subject: [PATCH 225/257] control: new file size upload limit --- src/main/java/org/kontalk/system/AttachmentManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index b7a91414..815ac00b 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -64,7 +64,7 @@ public class AttachmentManager implements Runnable { public static final Dimension THUMBNAIL_DIM = new Dimension(300, 200); public static final String THUMBNAIL_MIME = "image/jpeg"; - public static final int MAX_ATT_SIZE = 10 * 1024 * 1024; + public static final int MAX_ATT_SIZE = 20 * 1024 * 1024; public static class Slot { final URI uploadURL; From 12b1fd2607de3e4d4bdb4a5dbfbbfb6d0923a598 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 18:27:46 +0200 Subject: [PATCH 226/257] control: removed support for old Kontalk upload server That didnt survive long --- .../org/kontalk/system/AttachmentManager.java | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 815ac00b..5887c226 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -34,7 +34,6 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.kontalk.client.Client; -import org.kontalk.client.FeatureDiscovery; import org.kontalk.client.HTTPFileClient; import org.kontalk.crypto.Coder; import org.kontalk.crypto.Coder.Encryption; @@ -80,7 +79,6 @@ public Slot(URI uploadURI, URI downloadURL) { } } - private static final URI LEGACY_UPLOAD_URI = URI.create("https://beta.kontalk.net:5980/upload"); private static final String ENCRYPT_MIME = "application/octet-stream"; private final Control mControl; @@ -185,10 +183,6 @@ private void uploadAsync(OutMessage message) { } } - // use old file server or new upload service with XEP-0363? - boolean legacyUpload = !mClient.getServerFeature() - .contains(FeatureDiscovery.Feature.HTTP_FILE_UPLOAD); - // if text will be encrypted, always encrypt attachment too boolean encrypt = message.getCoderStatus().getEncryption() == Encryption.DECRYPTED; if (encrypt) { @@ -201,52 +195,35 @@ private void uploadAsync(OutMessage message) { if (encryptFile == null) return; file = encryptFile; - // NOTE: legacy upload service doesn't accept encrypted mime - if (!legacyUpload) { - mime = ENCRYPT_MIME; - } + mime = ENCRYPT_MIME; } - long length = file.length(); - HTTPFileClient client = this.clientOrNull(); if (client == null) return; - URI downloadURL; - if (legacyUpload) { - try { - downloadURL = client.uploadLegacy(file, LEGACY_UPLOAD_URI, mime, encrypt); - } catch (KonException ex) { - LOGGER.warning("legacy upload failed, attachment: "+attachment); - message.setStatus(KonMessage.Status.ERROR); - mControl.onException(ex); - return; - } - } else { - Slot uploadSlot = mClient.getUploadSlot(file.getName(), length, mime); - try { - client.upload(file, uploadSlot.uploadURL, mime, encrypt); - } catch (KonException ex) { - LOGGER.warning("upload failed, attachment: "+attachment); - message.setStatus(KonMessage.Status.ERROR); - mControl.onException(ex); - return; - } - downloadURL = uploadSlot.downloadURL; + long length = file.length(); + Slot uploadSlot = mClient.getUploadSlot(file.getName(), length, mime); + try { + client.upload(file, uploadSlot.uploadURL, mime, encrypt); + } catch (KonException ex) { + LOGGER.warning("upload failed, attachment: "+attachment); + message.setStatus(KonMessage.Status.ERROR); + mControl.onException(ex); + return; } if (!file.equals(original)) file.delete(); - if (downloadURL.toString().isEmpty()) { + if (uploadSlot.downloadURL.toString().isEmpty()) { LOGGER.warning("url empty: "+attachment); return; } - message.setUpload(downloadURL, mime, length); + message.setUpload(uploadSlot.downloadURL, mime, length); - LOGGER.info("upload successful, URL="+downloadURL); + LOGGER.info("upload successful, URL="+uploadSlot.downloadURL); // make sure not to loop if (attachment.hasURL()) From de2b093418f353fcbf17218ae8574119b42d11e8 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 18:29:32 +0200 Subject: [PATCH 227/257] view: correct server feature for file upload --- src/main/java/org/kontalk/view/ChatView.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 27fb7ac2..4cc8689c 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -374,7 +374,8 @@ void onStatusChange(Control.Status status, EnumSet ser switch(status) { case CONNECTED: this.setColor(Color.WHITE); - supported = serverFeature.contains(FeatureDiscovery.Feature.KON_FILE_UPLOAD); + supported = serverFeature.contains( + FeatureDiscovery.Feature.HTTP_FILE_UPLOAD); break; case DISCONNECTED: case ERROR: From e7db080a13857864377b690dd58976ce6ebd45b1 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 18:31:26 +0200 Subject: [PATCH 228/257] client: removed support for old Kontalk upload server --- .../org/kontalk/client/FeatureDiscovery.java | 3 -- .../org/kontalk/client/HTTPFileClient.java | 52 ++----------------- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/kontalk/client/FeatureDiscovery.java b/src/main/java/org/kontalk/client/FeatureDiscovery.java index 4490c456..7b884c21 100644 --- a/src/main/java/org/kontalk/client/FeatureDiscovery.java +++ b/src/main/java/org/kontalk/client/FeatureDiscovery.java @@ -46,8 +46,6 @@ public final class FeatureDiscovery { public enum Feature { USER_AVATAR, MULTI_ADDRESSING, - /** Old Kontalk upload service. */ - KON_FILE_UPLOAD, /** New XEP-0363 upload service. */ HTTP_FILE_UPLOAD } @@ -55,7 +53,6 @@ public enum Feature { static { FEATURE_MAP = new HashMap<>(); FEATURE_MAP.put(PubSub.NAMESPACE, Feature.USER_AVATAR); - FEATURE_MAP.put(HTTPFileClient.KON_UPLOAD_FEATURE, Feature.KON_FILE_UPLOAD); FEATURE_MAP.put(MultipleAddresses.NAMESPACE, Feature.MULTI_ADDRESSING); FEATURE_MAP.put(HTTPFileUpload.NAMESPACE, Feature.HTTP_FILE_UPLOAD); } diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index 6e2045d8..f9bd418f 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -24,7 +24,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -48,9 +47,7 @@ import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.InputStreamEntity; @@ -231,35 +228,22 @@ protected synchronized void afterWrite(int n) { * Upload file using a PUT request. * @return the URL the file can be downloaded with. */ - public void upload(File file, URI uploadURL, String mime, boolean encrypted) throws KonException { - this.upload(file, uploadURL, mime, encrypted, false); - } - - /** - * Upload file using a POST request and parse download URL from response. - * @return the URL the uploaded file can be downloaded with. - */ - public URI uploadLegacy(File file, URI url, String mime, boolean encrypted) throws KonException { - return this.upload(file, url, mime, encrypted, true); - } - - private URI upload(File file, URI url, String mime, boolean encrypted, boolean legacy) + public void upload(File file, URI uploadURL, String mime, boolean encrypted) throws KonException { + if (mHTTPClient == null) { mHTTPClient = httpClientOrNull(mPrivateKey, mCertificate, mValidateCertificate); if (mHTTPClient == null) throw new KonException(KonException.Error.UPLOAD_CREATE); } - // request type - HttpEntityEnclosingRequestBase req = legacy ? - new HttpPost(url) : - new HttpPut(url); + // request + HttpPut req = new HttpPut(uploadURL); req.setHeader("Content-Type", mime); if (encrypted) req.addHeader(HEADER_MESSAGE_FLAGS, "encrypted"); - LOGGER.config("to URL=" + url+ " ..."); + LOGGER.config("to URL=" + uploadURL+ " ..."); // execute request CloseableHttpResponse response; @@ -275,31 +259,12 @@ private URI upload(File file, URI url, String mime, boolean encrypted, boolean l throw new KonException(KonException.Error.UPLOAD_EXECUTE); } - // get URL from response entity - String downloadURL; try { int code = response.getStatusLine().getStatusCode(); if (code != HttpStatus.SC_OK) { LOGGER.warning("unexpected response code: " + code); throw new KonException(KonException.Error.UPLOAD_RESPONSE); } - - if (!legacy) - return null; - - HttpEntity entity = response.getEntity(); - if (entity == null) { - LOGGER.warning("no upload response entity"); - throw new KonException(KonException.Error.UPLOAD_RESPONSE); - } - - downloadURL = EntityUtils.toString(entity); - - // release http connection resource - EntityUtils.consume(entity); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't get url from response", ex); - throw new KonException(KonException.Error.UPLOAD_RESPONSE); } finally { try { response.close(); @@ -307,13 +272,6 @@ private URI upload(File file, URI url, String mime, boolean encrypted, boolean l LOGGER.log(Level.WARNING, "can't close response", ex); } } - - try { - return new URI(downloadURL); - } catch (URISyntaxException ex) { - LOGGER.log(Level.WARNING, "can't parse URI", ex); - throw new KonException(KonException.Error.UPLOAD_RESPONSE); - } } private static CloseableHttpClient httpClientOrNull(PrivateKey privateKey, From 839a61b55137e86c0a5bbd65e96a23bdd2327dce Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Fri, 22 Apr 2016 18:36:34 +0200 Subject: [PATCH 229/257] control: fixed attachmend upload handling when not connected --- .../org/kontalk/system/AttachmentManager.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 5887c226..bd8c8c30 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -152,6 +152,11 @@ private void uploadAsync(OutMessage message) { return; } + if (!mClient.isConnected()) { + LOGGER.info("can't upload, not connected"); + return; + } + File original; File file = original = attachment.getFilePath().toFile(); String mime = attachment.getMimeType(); @@ -204,6 +209,13 @@ private void uploadAsync(OutMessage message) { long length = file.length(); Slot uploadSlot = mClient.getUploadSlot(file.getName(), length, mime); + + if (uploadSlot.uploadURL.toString().isEmpty() || + uploadSlot.downloadURL.toString().isEmpty()) { + LOGGER.warning("empty slot: "+attachment); + return; + } + try { client.upload(file, uploadSlot.uploadURL, mime, encrypt); } catch (KonException ex) { @@ -216,11 +228,6 @@ private void uploadAsync(OutMessage message) { if (!file.equals(original)) file.delete(); - if (uploadSlot.downloadURL.toString().isEmpty()) { - LOGGER.warning("url empty: "+attachment); - return; - } - message.setUpload(uploadSlot.downloadURL, mime, length); LOGGER.info("upload successful, URL="+uploadSlot.downloadURL); From 5def63eee45840d7d3cb814d810092309e2c0c83 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 24 Apr 2016 21:41:29 +0200 Subject: [PATCH 230/257] view: show XMPP thread ID of chat in chat details XMPP Ids for threads are not used in Kontalk, but other clients do. At least Xabber always uses it for new chats/threads. --- src/main/java/org/kontalk/view/ChatDetails.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/org/kontalk/view/ChatDetails.java b/src/main/java/org/kontalk/view/ChatDetails.java index 236bd78d..e983941e 100644 --- a/src/main/java/org/kontalk/view/ChatDetails.java +++ b/src/main/java/org/kontalk/view/ChatDetails.java @@ -27,6 +27,8 @@ import com.alee.laf.radiobutton.WebRadioButton; import com.alee.laf.separator.WebSeparator; import com.alee.laf.slider.WebSlider; +import com.alee.laf.text.WebTextArea; +import com.alee.managers.tooltip.TooltipManager; import com.alee.utils.swing.UnselectableButtonGroup; import java.awt.BorderLayout; import java.awt.Color; @@ -38,6 +40,7 @@ import java.util.List; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import org.apache.commons.lang.StringUtils; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; @@ -147,6 +150,19 @@ public void itemStateChanged(ItemEvent e) { UnselectableButtonGroup.group(mColorOpt, mImgOpt); groupPanel.add(new WebSeparator()); + String xmppID = mChat.getXMPPID(); + if (!xmppID.isEmpty()) { + WebTextArea xmppIDArea = new WebTextArea().setBoldFont(); + xmppIDArea.setEditable(false); + xmppIDArea.setOpaque(false); + xmppIDArea.setText(StringUtils.abbreviate(xmppID, 30)); + TooltipManager.addTooltip(xmppIDArea, + Tr.tr("XMPP chat ID:") + " " + xmppID); + WebLabel xmppIDLabel = new WebLabel(Tr.tr("Chat ID:")); + groupPanel.add(new GroupPanel(View.GAP_DEFAULT, + xmppIDLabel, xmppIDArea)); + } + final WebButton saveButton = new WebButton(Tr.tr("Save")); this.add(groupPanel, BorderLayout.CENTER); From df35675841c8697936062c79fb16708812b27860 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sun, 24 Apr 2016 21:58:30 +0200 Subject: [PATCH 231/257] client-common-java: back to master! And using SMACK 1.7 now --- client-common-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common-java b/client-common-java index ad48809b..25d6e8ff 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit ad48809b66cbb32bc3e4cc3249150095253fdbfc +Subproject commit 25d6e8ff5e6eb7e40e2cc4ecc2ffb858bf137ea8 From 74b2c03d14a071cd273dfd8a0eab659fbf261e4b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Apr 2016 19:43:41 +0200 Subject: [PATCH 232/257] client: safer error handling when transfering files --- .../org/kontalk/client/HTTPFileClient.java | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index f9bd418f..7c79650b 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -50,6 +50,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.utils.HttpClientUtils; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -110,7 +111,8 @@ public void abort() { * @param base base directory in which the download is saved * @return absolute path of downloaded file, empty if download failed */ - public Path download(URI url, Path base, ProgressListener listener) throws KonException { + public synchronized Path download(URI url, Path base, ProgressListener listener) + throws KonException { if (mHTTPClient == null) { mHTTPClient = httpClientOrNull(mPrivateKey, mCertificate, mValidateCertificate); if (mHTTPClient == null) @@ -122,15 +124,15 @@ public Path download(URI url, Path base, ProgressListener listener) throws KonEx mCurrentListener = listener; // execute request - CloseableHttpResponse response; + CloseableHttpResponse response = null; try { - response = mHTTPClient.execute(mCurrentRequest); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't execute request", ex); - throw new KonException(KonException.Error.DOWNLOAD_EXECUTE); - } + try { + response = mHTTPClient.execute(mCurrentRequest); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't execute request", ex); + throw new KonException(KonException.Error.DOWNLOAD_EXECUTE); + } - try { int code = response.getStatusLine().getStatusCode(); if (code != HttpStatus.SC_OK) { LOGGER.warning("unexpected response code: " + code); @@ -209,18 +211,11 @@ protected synchronized void afterWrite(int n) { // release http connection resource EntityUtils.consumeQuietly(entity); - // TODO - mCurrentRequest = null; - mCurrentListener = null; - return destination.toAbsolutePath(); } finally { - try { - response.close(); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't close response", ex); - // TODO can't use this client anymore(?) - } + HttpClientUtils.closeQuietly(response); + mCurrentRequest = null; + mCurrentListener = null; } } @@ -228,7 +223,7 @@ protected synchronized void afterWrite(int n) { * Upload file using a PUT request. * @return the URL the file can be downloaded with. */ - public void upload(File file, URI uploadURL, String mime, boolean encrypted) + public synchronized void upload(File file, URI uploadURL, String mime, boolean encrypted) throws KonException { if (mHTTPClient == null) { @@ -246,31 +241,28 @@ public void upload(File file, URI uploadURL, String mime, boolean encrypted) LOGGER.config("to URL=" + uploadURL+ " ..."); // execute request - CloseableHttpResponse response; - try(FileInputStream in = new FileInputStream(file)) { - req.setEntity(new InputStreamEntity(in, file.length())); + CloseableHttpResponse response = null; + try { + try(FileInputStream in = new FileInputStream(file)) { + req.setEntity(new InputStreamEntity(in, file.length())); - mCurrentRequest = req; + mCurrentRequest = req; - //response = execute(currentRequest); - response = mHTTPClient.execute(mCurrentRequest); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't upload file", ex); - throw new KonException(KonException.Error.UPLOAD_EXECUTE); - } + //response = execute(currentRequest); + response = mHTTPClient.execute(mCurrentRequest); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't upload file", ex); + throw new KonException(KonException.Error.UPLOAD_EXECUTE); + } - try { int code = response.getStatusLine().getStatusCode(); if (code != HttpStatus.SC_OK) { LOGGER.warning("unexpected response code: " + code); throw new KonException(KonException.Error.UPLOAD_RESPONSE); } } finally { - try { - response.close(); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't close response", ex); - } + HttpClientUtils.closeQuietly(response); + mCurrentRequest = null; } } From 3cc4522a5daa23da2768acdc71dec4b6cbc1dddc Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 25 Apr 2016 19:55:29 +0200 Subject: [PATCH 233/257] copyright update --- src/main/java/org/kontalk/Kontalk.java | 2 +- src/main/java/org/kontalk/client/AcknowledgedListener.java | 2 +- src/main/java/org/kontalk/client/AvatarSendReceiver.java | 2 +- src/main/java/org/kontalk/client/BlockListListener.java | 2 +- src/main/java/org/kontalk/client/BlockSendReceiver.java | 2 +- src/main/java/org/kontalk/client/Client.java | 2 +- src/main/java/org/kontalk/client/EndpointServer.java | 2 +- src/main/java/org/kontalk/client/HKPClient.java | 2 +- src/main/java/org/kontalk/client/HTTPFileClient.java | 2 +- src/main/java/org/kontalk/client/KonConnection.java | 2 +- src/main/java/org/kontalk/client/KonConnectionListener.java | 2 +- src/main/java/org/kontalk/client/KonMessageListener.java | 2 +- src/main/java/org/kontalk/client/KonMessageSender.java | 2 +- src/main/java/org/kontalk/client/KonRosterListener.java | 2 +- src/main/java/org/kontalk/client/PresenceListener.java | 2 +- src/main/java/org/kontalk/client/PrivateKeyReceiver.java | 2 +- src/main/java/org/kontalk/client/PublicKeyListener.java | 2 +- src/main/java/org/kontalk/crypto/Coder.java | 2 +- src/main/java/org/kontalk/crypto/Decryptor.java | 2 +- src/main/java/org/kontalk/crypto/Encryptor.java | 2 +- src/main/java/org/kontalk/crypto/PGPUtils.java | 2 +- src/main/java/org/kontalk/crypto/PersonalKey.java | 2 +- src/main/java/org/kontalk/crypto/X509Bridge.java | 2 +- src/main/java/org/kontalk/misc/Callback.java | 2 +- src/main/java/org/kontalk/misc/JID.java | 2 +- src/main/java/org/kontalk/misc/KonException.java | 2 +- src/main/java/org/kontalk/misc/ViewEvent.java | 2 +- src/main/java/org/kontalk/model/Account.java | 2 +- src/main/java/org/kontalk/model/Avatar.java | 2 +- src/main/java/org/kontalk/model/Contact.java | 2 +- src/main/java/org/kontalk/model/ContactList.java | 2 +- src/main/java/org/kontalk/model/chat/Chat.java | 2 +- src/main/java/org/kontalk/model/chat/ChatList.java | 2 +- src/main/java/org/kontalk/model/chat/ChatMessages.java | 2 +- src/main/java/org/kontalk/model/chat/GroupChat.java | 2 +- src/main/java/org/kontalk/model/chat/GroupMetaData.java | 2 +- src/main/java/org/kontalk/model/chat/SingleChat.java | 2 +- src/main/java/org/kontalk/model/message/CoderStatus.java | 2 +- src/main/java/org/kontalk/model/message/DecryptMessage.java | 2 +- src/main/java/org/kontalk/model/message/InMessage.java | 2 +- src/main/java/org/kontalk/model/message/KonMessage.java | 2 +- src/main/java/org/kontalk/model/message/MessageContent.java | 2 +- src/main/java/org/kontalk/model/message/OutMessage.java | 2 +- src/main/java/org/kontalk/model/message/ProtoMessage.java | 2 +- src/main/java/org/kontalk/model/message/Transmission.java | 2 +- src/main/java/org/kontalk/persistence/Config.java | 2 +- src/main/java/org/kontalk/persistence/Database.java | 2 +- src/main/java/org/kontalk/system/AccountImporter.java | 2 +- src/main/java/org/kontalk/system/AttachmentManager.java | 2 +- src/main/java/org/kontalk/system/AvatarHandler.java | 2 +- src/main/java/org/kontalk/system/ChatStateManager.java | 2 +- src/main/java/org/kontalk/system/Control.java | 2 +- src/main/java/org/kontalk/system/GroupControl.java | 2 +- src/main/java/org/kontalk/system/RosterHandler.java | 2 +- src/main/java/org/kontalk/util/ClientUtils.java | 2 +- src/main/java/org/kontalk/util/CryptoUtils.java | 2 +- src/main/java/org/kontalk/util/EncodingUtils.java | 2 +- src/main/java/org/kontalk/util/MediaUtils.java | 2 +- src/main/java/org/kontalk/util/Tr.java | 2 +- src/main/java/org/kontalk/util/TrustUtils.java | 2 +- src/main/java/org/kontalk/util/XMPPUtils.java | 2 +- src/main/java/org/kontalk/view/AvatarLoader.java | 2 +- src/main/java/org/kontalk/view/ChatDetails.java | 2 +- src/main/java/org/kontalk/view/ChatListView.java | 2 +- src/main/java/org/kontalk/view/ChatView.java | 2 +- src/main/java/org/kontalk/view/ComponentUtils.java | 2 +- src/main/java/org/kontalk/view/ConfigurationDialog.java | 2 +- src/main/java/org/kontalk/view/ContactDetails.java | 2 +- src/main/java/org/kontalk/view/ContactListView.java | 2 +- src/main/java/org/kontalk/view/Content.java | 2 +- src/main/java/org/kontalk/view/ImageLoader.java | 2 +- src/main/java/org/kontalk/view/ImportDialog.java | 2 +- src/main/java/org/kontalk/view/LinkUtils.java | 2 +- src/main/java/org/kontalk/view/ListView.java | 2 +- src/main/java/org/kontalk/view/MainFrame.java | 2 +- src/main/java/org/kontalk/view/MessageList.java | 2 +- src/main/java/org/kontalk/view/Notifier.java | 2 +- src/main/java/org/kontalk/view/ProfileDialog.java | 2 +- src/main/java/org/kontalk/view/SearchPanel.java | 2 +- src/main/java/org/kontalk/view/TrayManager.java | 2 +- src/main/java/org/kontalk/view/Utils.java | 2 +- src/main/java/org/kontalk/view/View.java | 2 +- src/test/java/org/kontalk/KontalkTest.java | 2 +- src/test/java/org/kontalk/util/CryptoUtilsTest.java | 2 +- 84 files changed, 84 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index 362a90c2..afbae0bb 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/AcknowledgedListener.java b/src/main/java/org/kontalk/client/AcknowledgedListener.java index c4d05d5d..5f170f03 100644 --- a/src/main/java/org/kontalk/client/AcknowledgedListener.java +++ b/src/main/java/org/kontalk/client/AcknowledgedListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/AvatarSendReceiver.java b/src/main/java/org/kontalk/client/AvatarSendReceiver.java index 141a33c8..fb441a88 100644 --- a/src/main/java/org/kontalk/client/AvatarSendReceiver.java +++ b/src/main/java/org/kontalk/client/AvatarSendReceiver.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/BlockListListener.java b/src/main/java/org/kontalk/client/BlockListListener.java index 6197b451..42dd2bdf 100644 --- a/src/main/java/org/kontalk/client/BlockListListener.java +++ b/src/main/java/org/kontalk/client/BlockListListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/BlockSendReceiver.java b/src/main/java/org/kontalk/client/BlockSendReceiver.java index 1209141d..65130aec 100644 --- a/src/main/java/org/kontalk/client/BlockSendReceiver.java +++ b/src/main/java/org/kontalk/client/BlockSendReceiver.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/Client.java b/src/main/java/org/kontalk/client/Client.java index 47649534..246778ef 100644 --- a/src/main/java/org/kontalk/client/Client.java +++ b/src/main/java/org/kontalk/client/Client.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/EndpointServer.java b/src/main/java/org/kontalk/client/EndpointServer.java index 9030d4fe..72024834 100644 --- a/src/main/java/org/kontalk/client/EndpointServer.java +++ b/src/main/java/org/kontalk/client/EndpointServer.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/HKPClient.java b/src/main/java/org/kontalk/client/HKPClient.java index 2d8edeaa..383dccb3 100644 --- a/src/main/java/org/kontalk/client/HKPClient.java +++ b/src/main/java/org/kontalk/client/HKPClient.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/HTTPFileClient.java b/src/main/java/org/kontalk/client/HTTPFileClient.java index 7c79650b..d95cf30d 100644 --- a/src/main/java/org/kontalk/client/HTTPFileClient.java +++ b/src/main/java/org/kontalk/client/HTTPFileClient.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/KonConnection.java b/src/main/java/org/kontalk/client/KonConnection.java index f95c5bd4..dc86e11b 100644 --- a/src/main/java/org/kontalk/client/KonConnection.java +++ b/src/main/java/org/kontalk/client/KonConnection.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/KonConnectionListener.java b/src/main/java/org/kontalk/client/KonConnectionListener.java index 626bd68c..b4a6a88e 100644 --- a/src/main/java/org/kontalk/client/KonConnectionListener.java +++ b/src/main/java/org/kontalk/client/KonConnectionListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/KonMessageListener.java b/src/main/java/org/kontalk/client/KonMessageListener.java index 72bbb625..64602e94 100644 --- a/src/main/java/org/kontalk/client/KonMessageListener.java +++ b/src/main/java/org/kontalk/client/KonMessageListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/KonMessageSender.java b/src/main/java/org/kontalk/client/KonMessageSender.java index 0f2fb9ff..d0d4d469 100644 --- a/src/main/java/org/kontalk/client/KonMessageSender.java +++ b/src/main/java/org/kontalk/client/KonMessageSender.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/KonRosterListener.java b/src/main/java/org/kontalk/client/KonRosterListener.java index d7715000..b5888b9a 100644 --- a/src/main/java/org/kontalk/client/KonRosterListener.java +++ b/src/main/java/org/kontalk/client/KonRosterListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/PresenceListener.java b/src/main/java/org/kontalk/client/PresenceListener.java index 9dcb65b5..8ea7a2f3 100644 --- a/src/main/java/org/kontalk/client/PresenceListener.java +++ b/src/main/java/org/kontalk/client/PresenceListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/PrivateKeyReceiver.java b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java index 2d436532..d175c0e2 100644 --- a/src/main/java/org/kontalk/client/PrivateKeyReceiver.java +++ b/src/main/java/org/kontalk/client/PrivateKeyReceiver.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/client/PublicKeyListener.java b/src/main/java/org/kontalk/client/PublicKeyListener.java index 986eb0de..efc5a8f3 100644 --- a/src/main/java/org/kontalk/client/PublicKeyListener.java +++ b/src/main/java/org/kontalk/client/PublicKeyListener.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/Coder.java b/src/main/java/org/kontalk/crypto/Coder.java index 77554d57..6baf3d97 100644 --- a/src/main/java/org/kontalk/crypto/Coder.java +++ b/src/main/java/org/kontalk/crypto/Coder.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 2804420c..65a234a6 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/Encryptor.java b/src/main/java/org/kontalk/crypto/Encryptor.java index 5cd4b848..7b58ce6d 100644 --- a/src/main/java/org/kontalk/crypto/Encryptor.java +++ b/src/main/java/org/kontalk/crypto/Encryptor.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/PGPUtils.java b/src/main/java/org/kontalk/crypto/PGPUtils.java index ff166d56..761e50f3 100644 --- a/src/main/java/org/kontalk/crypto/PGPUtils.java +++ b/src/main/java/org/kontalk/crypto/PGPUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/PersonalKey.java b/src/main/java/org/kontalk/crypto/PersonalKey.java index ec89941c..51bb87b4 100644 --- a/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/crypto/X509Bridge.java b/src/main/java/org/kontalk/crypto/X509Bridge.java index 21df84e7..972bbdfa 100644 --- a/src/main/java/org/kontalk/crypto/X509Bridge.java +++ b/src/main/java/org/kontalk/crypto/X509Bridge.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/misc/Callback.java b/src/main/java/org/kontalk/misc/Callback.java index dfdaccf0..b39fb932 100644 --- a/src/main/java/org/kontalk/misc/Callback.java +++ b/src/main/java/org/kontalk/misc/Callback.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 48f1cca2..9d1e88c2 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/misc/KonException.java b/src/main/java/org/kontalk/misc/KonException.java index 0c42429d..acfb11d7 100644 --- a/src/main/java/org/kontalk/misc/KonException.java +++ b/src/main/java/org/kontalk/misc/KonException.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/misc/ViewEvent.java b/src/main/java/org/kontalk/misc/ViewEvent.java index 62ada4b5..1ee0ed68 100644 --- a/src/main/java/org/kontalk/misc/ViewEvent.java +++ b/src/main/java/org/kontalk/misc/ViewEvent.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index 6c992df6..8233f483 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/Avatar.java b/src/main/java/org/kontalk/model/Avatar.java index e4dec2ba..47a4a581 100644 --- a/src/main/java/org/kontalk/model/Avatar.java +++ b/src/main/java/org/kontalk/model/Avatar.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 7a9c21f6..d631f573 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 6f1861e0..108305c7 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/Chat.java b/src/main/java/org/kontalk/model/chat/Chat.java index 4b324472..032a34b8 100644 --- a/src/main/java/org/kontalk/model/chat/Chat.java +++ b/src/main/java/org/kontalk/model/chat/Chat.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/ChatList.java b/src/main/java/org/kontalk/model/chat/ChatList.java index a83abadf..09003e67 100644 --- a/src/main/java/org/kontalk/model/chat/ChatList.java +++ b/src/main/java/org/kontalk/model/chat/ChatList.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/ChatMessages.java b/src/main/java/org/kontalk/model/chat/ChatMessages.java index 1c0042a7..99e8071c 100644 --- a/src/main/java/org/kontalk/model/chat/ChatMessages.java +++ b/src/main/java/org/kontalk/model/chat/ChatMessages.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/GroupChat.java b/src/main/java/org/kontalk/model/chat/GroupChat.java index 7e6b9a1e..cfac9739 100644 --- a/src/main/java/org/kontalk/model/chat/GroupChat.java +++ b/src/main/java/org/kontalk/model/chat/GroupChat.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/GroupMetaData.java b/src/main/java/org/kontalk/model/chat/GroupMetaData.java index 561971c1..373e1fc4 100644 --- a/src/main/java/org/kontalk/model/chat/GroupMetaData.java +++ b/src/main/java/org/kontalk/model/chat/GroupMetaData.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index 7e4f6ca7..76076d99 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/CoderStatus.java b/src/main/java/org/kontalk/model/message/CoderStatus.java index 07aac217..2edca669 100644 --- a/src/main/java/org/kontalk/model/message/CoderStatus.java +++ b/src/main/java/org/kontalk/model/message/CoderStatus.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/DecryptMessage.java b/src/main/java/org/kontalk/model/message/DecryptMessage.java index bdd51c6d..c7d0aeaa 100644 --- a/src/main/java/org/kontalk/model/message/DecryptMessage.java +++ b/src/main/java/org/kontalk/model/message/DecryptMessage.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/InMessage.java b/src/main/java/org/kontalk/model/message/InMessage.java index 68904f12..78bd0ecc 100644 --- a/src/main/java/org/kontalk/model/message/InMessage.java +++ b/src/main/java/org/kontalk/model/message/InMessage.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/KonMessage.java b/src/main/java/org/kontalk/model/message/KonMessage.java index 73204c69..18113495 100644 --- a/src/main/java/org/kontalk/model/message/KonMessage.java +++ b/src/main/java/org/kontalk/model/message/KonMessage.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index 747c4e53..bf1c3404 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/OutMessage.java b/src/main/java/org/kontalk/model/message/OutMessage.java index 8a26a45b..e44db314 100644 --- a/src/main/java/org/kontalk/model/message/OutMessage.java +++ b/src/main/java/org/kontalk/model/message/OutMessage.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/ProtoMessage.java b/src/main/java/org/kontalk/model/message/ProtoMessage.java index 425b9d76..d265e59a 100644 --- a/src/main/java/org/kontalk/model/message/ProtoMessage.java +++ b/src/main/java/org/kontalk/model/message/ProtoMessage.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/model/message/Transmission.java b/src/main/java/org/kontalk/model/message/Transmission.java index 31e8b107..46500d5b 100644 --- a/src/main/java/org/kontalk/model/message/Transmission.java +++ b/src/main/java/org/kontalk/model/message/Transmission.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/persistence/Config.java b/src/main/java/org/kontalk/persistence/Config.java index 86e8f27e..8f05c0d7 100644 --- a/src/main/java/org/kontalk/persistence/Config.java +++ b/src/main/java/org/kontalk/persistence/Config.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/persistence/Database.java b/src/main/java/org/kontalk/persistence/Database.java index b62b2cda..6037aa62 100644 --- a/src/main/java/org/kontalk/persistence/Database.java +++ b/src/main/java/org/kontalk/persistence/Database.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/AccountImporter.java b/src/main/java/org/kontalk/system/AccountImporter.java index 81bab432..4d76012d 100644 --- a/src/main/java/org/kontalk/system/AccountImporter.java +++ b/src/main/java/org/kontalk/system/AccountImporter.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index bd8c8c30..a41671fb 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/AvatarHandler.java b/src/main/java/org/kontalk/system/AvatarHandler.java index 5df3471c..c1837070 100644 --- a/src/main/java/org/kontalk/system/AvatarHandler.java +++ b/src/main/java/org/kontalk/system/AvatarHandler.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/ChatStateManager.java b/src/main/java/org/kontalk/system/ChatStateManager.java index ec3d32bb..a0b7eea6 100644 --- a/src/main/java/org/kontalk/system/ChatStateManager.java +++ b/src/main/java/org/kontalk/system/ChatStateManager.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 1aec432b..1d520f8d 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/GroupControl.java b/src/main/java/org/kontalk/system/GroupControl.java index 26882635..c472b11b 100644 --- a/src/main/java/org/kontalk/system/GroupControl.java +++ b/src/main/java/org/kontalk/system/GroupControl.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index 4c8757bd..f9c5d564 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index f5040680..361de399 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/CryptoUtils.java b/src/main/java/org/kontalk/util/CryptoUtils.java index e869c0e0..537fc42c 100644 --- a/src/main/java/org/kontalk/util/CryptoUtils.java +++ b/src/main/java/org/kontalk/util/CryptoUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/EncodingUtils.java b/src/main/java/org/kontalk/util/EncodingUtils.java index 2f018587..fe43faaf 100644 --- a/src/main/java/org/kontalk/util/EncodingUtils.java +++ b/src/main/java/org/kontalk/util/EncodingUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 24bb7760..330ff03f 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/Tr.java b/src/main/java/org/kontalk/util/Tr.java index e188f79c..607e122e 100644 --- a/src/main/java/org/kontalk/util/Tr.java +++ b/src/main/java/org/kontalk/util/Tr.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/TrustUtils.java b/src/main/java/org/kontalk/util/TrustUtils.java index 7cc2e3bb..da819a6b 100644 --- a/src/main/java/org/kontalk/util/TrustUtils.java +++ b/src/main/java/org/kontalk/util/TrustUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/util/XMPPUtils.java b/src/main/java/org/kontalk/util/XMPPUtils.java index 2696d58d..dbfff0bd 100644 --- a/src/main/java/org/kontalk/util/XMPPUtils.java +++ b/src/main/java/org/kontalk/util/XMPPUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index ab27f71c..7e7b3d96 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ChatDetails.java b/src/main/java/org/kontalk/view/ChatDetails.java index e983941e..9827c3d3 100644 --- a/src/main/java/org/kontalk/view/ChatDetails.java +++ b/src/main/java/org/kontalk/view/ChatDetails.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index e6d29c9f..baa3d2f2 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ChatView.java b/src/main/java/org/kontalk/view/ChatView.java index 4cc8689c..264f84f6 100644 --- a/src/main/java/org/kontalk/view/ChatView.java +++ b/src/main/java/org/kontalk/view/ChatView.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ComponentUtils.java b/src/main/java/org/kontalk/view/ComponentUtils.java index 3b30c4b4..1897aaa2 100644 --- a/src/main/java/org/kontalk/view/ComponentUtils.java +++ b/src/main/java/org/kontalk/view/ComponentUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ConfigurationDialog.java b/src/main/java/org/kontalk/view/ConfigurationDialog.java index 29a83bd9..b6664365 100644 --- a/src/main/java/org/kontalk/view/ConfigurationDialog.java +++ b/src/main/java/org/kontalk/view/ConfigurationDialog.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ContactDetails.java b/src/main/java/org/kontalk/view/ContactDetails.java index 7b5fc9f9..406974ef 100644 --- a/src/main/java/org/kontalk/view/ContactDetails.java +++ b/src/main/java/org/kontalk/view/ContactDetails.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index afdc916f..457fb68b 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/Content.java b/src/main/java/org/kontalk/view/Content.java index ed196505..a4f4a191 100644 --- a/src/main/java/org/kontalk/view/Content.java +++ b/src/main/java/org/kontalk/view/Content.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ImageLoader.java b/src/main/java/org/kontalk/view/ImageLoader.java index 849ceed7..64bc3939 100644 --- a/src/main/java/org/kontalk/view/ImageLoader.java +++ b/src/main/java/org/kontalk/view/ImageLoader.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ImportDialog.java b/src/main/java/org/kontalk/view/ImportDialog.java index f4249de1..94313c61 100644 --- a/src/main/java/org/kontalk/view/ImportDialog.java +++ b/src/main/java/org/kontalk/view/ImportDialog.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/LinkUtils.java b/src/main/java/org/kontalk/view/LinkUtils.java index 49204fc7..0ad0d6e1 100644 --- a/src/main/java/org/kontalk/view/LinkUtils.java +++ b/src/main/java/org/kontalk/view/LinkUtils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index 3184f55e..b9357078 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/MainFrame.java b/src/main/java/org/kontalk/view/MainFrame.java index 9e5a2c3a..c16262de 100644 --- a/src/main/java/org/kontalk/view/MainFrame.java +++ b/src/main/java/org/kontalk/view/MainFrame.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/MessageList.java b/src/main/java/org/kontalk/view/MessageList.java index c32cab85..0c82a9f4 100644 --- a/src/main/java/org/kontalk/view/MessageList.java +++ b/src/main/java/org/kontalk/view/MessageList.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/Notifier.java b/src/main/java/org/kontalk/view/Notifier.java index 995f031e..4d8824f0 100644 --- a/src/main/java/org/kontalk/view/Notifier.java +++ b/src/main/java/org/kontalk/view/Notifier.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/ProfileDialog.java b/src/main/java/org/kontalk/view/ProfileDialog.java index 838b5607..6cc968a4 100644 --- a/src/main/java/org/kontalk/view/ProfileDialog.java +++ b/src/main/java/org/kontalk/view/ProfileDialog.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/SearchPanel.java b/src/main/java/org/kontalk/view/SearchPanel.java index 8e4b0f8d..0b901667 100644 --- a/src/main/java/org/kontalk/view/SearchPanel.java +++ b/src/main/java/org/kontalk/view/SearchPanel.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/TrayManager.java b/src/main/java/org/kontalk/view/TrayManager.java index 40a0a586..b311a216 100644 --- a/src/main/java/org/kontalk/view/TrayManager.java +++ b/src/main/java/org/kontalk/view/TrayManager.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 9aaac72b..e4492670 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/main/java/org/kontalk/view/View.java b/src/main/java/org/kontalk/view/View.java index ebd8d8b0..41d7d6eb 100644 --- a/src/main/java/org/kontalk/view/View.java +++ b/src/main/java/org/kontalk/view/View.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/test/java/org/kontalk/KontalkTest.java b/src/test/java/org/kontalk/KontalkTest.java index 89411909..1a7f36a8 100644 --- a/src/test/java/org/kontalk/KontalkTest.java +++ b/src/test/java/org/kontalk/KontalkTest.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/test/java/org/kontalk/util/CryptoUtilsTest.java b/src/test/java/org/kontalk/util/CryptoUtilsTest.java index 664fc1b3..4e7bb8f6 100644 --- a/src/test/java/org/kontalk/util/CryptoUtilsTest.java +++ b/src/test/java/org/kontalk/util/CryptoUtilsTest.java @@ -1,6 +1,6 @@ /* * Kontalk Java client - * Copyright (C) 2014 Kontalk Devteam + * Copyright (C) 2016 Kontalk Devteam * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From ccdefcf3fce82e89ec781f08f00f92a31ee92186 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Apr 2016 17:02:50 +0200 Subject: [PATCH 234/257] utils: improved error logging when playing ogg file Problem was an ALSA vs. PulseAudio fight on my machine. --- src/main/java/org/kontalk/util/OggClip.java | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/kontalk/util/OggClip.java b/src/main/java/org/kontalk/util/OggClip.java index fcc4de5d..43b31029 100644 --- a/src/main/java/org/kontalk/util/OggClip.java +++ b/src/main/java/org/kontalk/util/OggClip.java @@ -8,7 +8,6 @@ import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.FloatControl; -import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import com.jcraft.jogg.Packet; @@ -19,6 +18,8 @@ import com.jcraft.jorbis.Comment; import com.jcraft.jorbis.DspState; import com.jcraft.jorbis.Info; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Simple Clip like player for OGG's. Code is mostly taken from the example @@ -29,6 +30,7 @@ * @author kevin */ class OggClip { + private static final Logger LOGGER = Logger.getLogger(OggClip.class.getName()); private final int BUFSIZE = 4096 * 2; private int convsize = BUFSIZE * 2; @@ -331,23 +333,17 @@ private void initJavaSound(int channels, int rate) { throw new Exception("Line " + info + " not supported."); } - try { - outputLine = (SourceDataLine) AudioSystem.getLine(info); - // outputLine.addLineListener(this); - outputLine.open(audioFormat); - } catch (LineUnavailableException ex) { - throw new Exception("Unable to open the sourceDataLine: " + ex); - } catch (IllegalArgumentException ex) { - throw new Exception("Illegal Argument: " + ex); - } + outputLine = (SourceDataLine) AudioSystem.getLine(info); + // outputLine.addLineListener(this); + outputLine.open(audioFormat); this.rate = rate; this.channels = channels; setBalance(balance); setGain(gain); - } catch (Exception ee) { - System.out.println(ee); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, "can not init sound system", ex); } } From 94df32c3138cbc528010598514fbf1c78b26247c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Apr 2016 17:04:24 +0200 Subject: [PATCH 235/257] utils: line encoding in OggClip --- src/main/java/org/kontalk/util/OggClip.java | 1244 +++++++++---------- 1 file changed, 622 insertions(+), 622 deletions(-) diff --git a/src/main/java/org/kontalk/util/OggClip.java b/src/main/java/org/kontalk/util/OggClip.java index 43b31029..80edc962 100644 --- a/src/main/java/org/kontalk/util/OggClip.java +++ b/src/main/java/org/kontalk/util/OggClip.java @@ -1,622 +1,622 @@ -package org.kontalk.util; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; - -import javax.sound.sampled.AudioFormat; -import javax.sound.sampled.AudioSystem; -import javax.sound.sampled.DataLine; -import javax.sound.sampled.FloatControl; -import javax.sound.sampled.SourceDataLine; - -import com.jcraft.jogg.Packet; -import com.jcraft.jogg.Page; -import com.jcraft.jogg.StreamState; -import com.jcraft.jogg.SyncState; -import com.jcraft.jorbis.Block; -import com.jcraft.jorbis.Comment; -import com.jcraft.jorbis.DspState; -import com.jcraft.jorbis.Info; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Simple Clip like player for OGG's. Code is mostly taken from the example - * provided with JOrbis. - * - * Source: http://www.cokeandcode.com/main/code/ - * - * @author kevin - */ -class OggClip { - private static final Logger LOGGER = Logger.getLogger(OggClip.class.getName()); - - private final int BUFSIZE = 4096 * 2; - private int convsize = BUFSIZE * 2; - private byte[] convbuffer = new byte[convsize]; - private SyncState oy; - private StreamState os; - private Page og; - private Packet op; - private Info vi; - private Comment vc; - private DspState vd; - private Block vb; - private SourceDataLine outputLine; - private int rate; - private int channels; - private BufferedInputStream bitStream = null; - private byte[] buffer = null; - private int bytes = 0; - private Thread player = null; - - private float balance; - private float gain = -1; - private boolean paused; - private float oldGain; - - /** - * Create a new clip based on a reference into the class path - * - * @param ref The reference into the class path which the ogg can be read - * from - * @throws IOException Indicated a failure to find the resource - */ - public OggClip(String ref) throws IOException { - try { - init(Thread.currentThread().getContextClassLoader().getResourceAsStream(ref)); - } catch (IOException e) { - throw new IOException("Couldn't find: " + ref); - } - } - - /** - * Create a new clip based on a reference into the class path - * - * @param in The stream from which the ogg can be read from - * @throws IOException Indicated a failure to read from the stream - */ - public OggClip(InputStream in) throws IOException { - init(in); - } - - /** - * Set the default gain value (default volume) - */ - public void setDefaultGain() { - setGain(-1); - } - - /** - * Attempt to set the global gain (volume ish) for the play back. If the - * control is not supported this method has no effect. 1.0 will set maximum - * gain, 0.0 minimum gain - * - * @param gain The gain value - */ - public void setGain(float gain) { - if (gain != -1) { - if ((gain < 0) || (gain > 1)) { - throw new IllegalArgumentException("Volume must be between 0.0 and 1.0"); - } - } - - this.gain = gain; - - if (outputLine == null) { - return; - } - - try { - FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.MASTER_GAIN); - if (gain == -1) { - control.setValue(0); - } else { - float max = control.getMaximum(); - float min = control.getMinimum(); // negative values all seem to be zero? - float range = max - min; - - control.setValue(min + (range * gain)); - } - } catch (IllegalArgumentException e) { - // gain not supported - e.printStackTrace(); - } - } - - /** - * Attempt to set the balance between the two speakers. -1.0 is full left - * speak, 1.0 if full right speaker. Anywhere in between moves between the - * two speakers. If the control is not supported this method has no effect - * - * @param balance The balance value - */ - public void setBalance(float balance) { - this.balance = balance; - - if (outputLine == null) { - return; - } - - try { - FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.BALANCE); - control.setValue(balance); - } catch (IllegalArgumentException e) { - // balance not supported - } - } - - /** - * Check the state of the play back - * - * @return True if the playback has been stopped - */ - private boolean checkState() { - while (paused && (player != null)) { - synchronized (player) { - if (player != null) { - try { - player.wait(); - } catch (InterruptedException e) { - // ignored - } - } - } - } - - return stopped(); - } - - /** - * Pause the play back - */ - public void pause() { - paused = true; - oldGain = gain; - setGain(0); - } - - /** - * Check if the stream is paused - * - * @return True if the stream is paused - */ - public boolean isPaused() { - return paused; - } - - /** - * Resume the play back - */ - public void resume() { - if (!paused) { - play(); - return; - } - - paused = false; - - synchronized (player) { - if (player != null) { - player.notify(); - } - } - setGain(oldGain); - } - - /** - * Check if the clip has been stopped - * - * @return True if the clip has been stopped - */ - public boolean stopped() { - return ((player == null) || (!player.isAlive())); - } - - /** - * Initialise the ogg clip - * - * @param in The stream we're going to read from - * @throws IOException Indicates a failure to read from the stream - */ - private void init(InputStream in) throws IOException { - if (in == null) { - throw new IOException("Couldn't find input source"); - } - bitStream = new BufferedInputStream(in); - bitStream.mark(Integer.MAX_VALUE); - } - - /** - * Play the clip once - */ - public void play() { - stop(); - - try { - bitStream.reset(); - } catch (IOException e) { - // ignore if no mark - } - - player = new Thread("OGG Play") { - @Override - public void run() { - try { - playStream(Thread.currentThread()); - } catch (InternalException e) { - e.printStackTrace(); - } - - try { - bitStream.reset(); - } catch (IOException e) { - e.printStackTrace(); - } - }; - }; - player.setDaemon(true); - player.start(); - } - - /** - * Loop the clip - maybe for background music - */ - public void loop() { - stop(); - - try { - bitStream.reset(); - } catch (IOException e) { - // ignore if no mark - } - - player = new Thread("OGG Loop") { - @Override - public void run() { - while (player == Thread.currentThread()) { - try { - playStream(Thread.currentThread()); - } catch (InternalException e) { - e.printStackTrace(); - player = null; - } - - try { - bitStream.reset(); - } catch (IOException e) { - } - } - }; - }; - player.setDaemon(true); - player.start(); - } - - /** - * Stop the clip playing - */ - public void stop() { - if (stopped()) { - return; - } - - player = null; - outputLine.drain(); - } - - /** - * Close the stream being played from - */ - public void close() { - try { - if (bitStream != null) { - bitStream.close(); - } - } catch (IOException e) { - } - } - - /* - * Taken from the JOrbis Player - */ - private void initJavaSound(int channels, int rate) { - try { - AudioFormat audioFormat = new AudioFormat(rate, 16, - channels, true, // PCM_Signed - false // littleEndian - ); - DataLine.Info info = new DataLine.Info(SourceDataLine.class, - audioFormat, AudioSystem.NOT_SPECIFIED); - if (!AudioSystem.isLineSupported(info)) { - throw new Exception("Line " + info + " not supported."); - } - - outputLine = (SourceDataLine) AudioSystem.getLine(info); - // outputLine.addLineListener(this); - outputLine.open(audioFormat); - - this.rate = rate; - this.channels = channels; - - setBalance(balance); - setGain(gain); - } catch (Exception ex) { - LOGGER.log(Level.WARNING, "can not init sound system", ex); - } - } - - /* - * Taken from the JOrbis Player - */ - private SourceDataLine getOutputLine(int channels, int rate) { - if (outputLine == null || this.rate != rate - || this.channels != channels) { - if (outputLine != null) { - outputLine.drain(); - outputLine.stop(); - outputLine.close(); - } - initJavaSound(channels, rate); - outputLine.start(); - } - return outputLine; - } - - /* - * Taken from the JOrbis Player - */ - private void initJOrbis() { - oy = new SyncState(); - os = new StreamState(); - og = new Page(); - op = new Packet(); - - vi = new Info(); - vc = new Comment(); - vd = new DspState(); - vb = new Block(vd); - - buffer = null; - bytes = 0; - - oy.init(); - } - - /* - * Taken from the JOrbis Player - */ - private void playStream(Thread me) throws InternalException { - boolean chained = false; - - initJOrbis(); - - while (true) { - if (checkState()) { - return; - } - - int eos = 0; - - int index = oy.buffer(BUFSIZE); - buffer = oy.data; - try { - bytes = bitStream.read(buffer, index, BUFSIZE); - } catch (Exception e) { - throw new InternalException(e); - } - oy.wrote(bytes); - - if (chained) { - chained = false; - } else { - if (oy.pageout(og) != 1) { - if (bytes < BUFSIZE) { - break; - } - throw new InternalException("Input does not appear to be an Ogg bitstream."); - } - } - os.init(og.serialno()); - os.reset(); - - vi.init(); - vc.init(); - - if (os.pagein(og) < 0) { - // error; stream version mismatch perhaps - throw new InternalException("Error reading first page of Ogg bitstream data."); - } - - if (os.packetout(op) != 1) { - // no page? must not be vorbis - throw new InternalException("Error reading initial header packet."); - } - - if (vi.synthesis_headerin(vc, op) < 0) { - // error case; not a vorbis header - throw new InternalException("This Ogg bitstream does not contain Vorbis audio data."); - } - - int i = 0; - - while (i < 2) { - while (i < 2) { - if (checkState()) { - return; - } - - int result = oy.pageout(og); - if (result == 0) { - break; // Need more data - } - if (result == 1) { - os.pagein(og); - while (i < 2) { - result = os.packetout(op); - if (result == 0) { - break; - } - if (result == -1) { - throw new InternalException("Corrupt secondary header. Exiting."); - } - vi.synthesis_headerin(vc, op); - i++; - } - } - } - - index = oy.buffer(BUFSIZE); - buffer = oy.data; - try { - bytes = bitStream.read(buffer, index, BUFSIZE); - } catch (Exception e) { - throw new InternalException(e); - } - if (bytes == 0 && i < 2) { - throw new InternalException("End of file before finding all Vorbis headers!"); - } - oy.wrote(bytes); - } - - convsize = BUFSIZE / vi.channels; - - vd.synthesis_init(vi); - vb.init(vd); - - float[][][] _pcmf = new float[1][][]; - int[] _index = new int[vi.channels]; - - getOutputLine(vi.channels, vi.rate); - - while (eos == 0) { - while (eos == 0) { - if (player != me) { - return; - } - - int result = oy.pageout(og); - if (result == 0) { - break; // need more data - } - if (result == -1) { // missing or corrupt data at this page - // position - // System.err.println("Corrupt or missing data in - // bitstream; - // continuing..."); - } else { - os.pagein(og); - - if (og.granulepos() == 0) { // - chained = true; // - eos = 1; // - break; // - } // - - while (true) { - if (checkState()) { - return; - } - - result = os.packetout(op); - if (result == 0) { - break; // need more data - } - if (result == -1) { // missing or corrupt data at - // this page position - // no reason to complain; already complained - // above - - // System.err.println("no reason to complain; - // already complained above"); - } else { - // we have a packet. Decode it - int samples; - if (vb.synthesis(op) == 0) { // test for - // success! - vd.synthesis_blockin(vb); - } - while ((samples = vd.synthesis_pcmout(_pcmf, - _index)) > 0) { - if (checkState()) { - return; - } - - float[][] pcmf = _pcmf[0]; - int bout = (samples < convsize ? samples - : convsize); - - // convert doubles to 16 bit signed ints - // (host order) and - // interleave - for (i = 0; i < vi.channels; i++) { - int ptr = i * 2; - // int ptr=i; - int mono = _index[i]; - for (int j = 0; j < bout; j++) { - int val = (int) (pcmf[i][mono + j] * 32767.); - if (val > 32767) { - val = 32767; - } - if (val < -32768) { - val = -32768; - } - if (val < 0) { - val = val | 0x8000; - } - convbuffer[ptr] = (byte) (val); - convbuffer[ptr + 1] = (byte) (val >>> 8); - ptr += 2 * (vi.channels); - } - } - outputLine.write(convbuffer, 0, 2 - * vi.channels * bout); - vd.synthesis_read(bout); - } - } - } - if (og.eos() != 0) { - eos = 1; - } - } - } - - if (eos == 0) { - index = oy.buffer(BUFSIZE); - buffer = oy.data; - try { - bytes = bitStream.read(buffer, index, BUFSIZE); - } catch (Exception e) { - throw new InternalException(e); - } - if (bytes == -1) { - break; - } - oy.wrote(bytes); - if (bytes == 0) { - eos = 1; - } - } - } - - os.clear(); - vb.clear(); - vd.clear(); - vi.clear(); - } - - oy.clear(); - } - - private class InternalException extends Exception { - - public InternalException(Exception e) { - super(e); - } - - public InternalException(String msg) { - super(msg); - } - } -} +package org.kontalk.util; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.SourceDataLine; + +import com.jcraft.jogg.Packet; +import com.jcraft.jogg.Page; +import com.jcraft.jogg.StreamState; +import com.jcraft.jogg.SyncState; +import com.jcraft.jorbis.Block; +import com.jcraft.jorbis.Comment; +import com.jcraft.jorbis.DspState; +import com.jcraft.jorbis.Info; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Simple Clip like player for OGG's. Code is mostly taken from the example + * provided with JOrbis. + * + * Source: http://www.cokeandcode.com/main/code/ + * + * @author kevin + */ +class OggClip { + private static final Logger LOGGER = Logger.getLogger(OggClip.class.getName()); + + private final int BUFSIZE = 4096 * 2; + private int convsize = BUFSIZE * 2; + private byte[] convbuffer = new byte[convsize]; + private SyncState oy; + private StreamState os; + private Page og; + private Packet op; + private Info vi; + private Comment vc; + private DspState vd; + private Block vb; + private SourceDataLine outputLine; + private int rate; + private int channels; + private BufferedInputStream bitStream = null; + private byte[] buffer = null; + private int bytes = 0; + private Thread player = null; + + private float balance; + private float gain = -1; + private boolean paused; + private float oldGain; + + /** + * Create a new clip based on a reference into the class path + * + * @param ref The reference into the class path which the ogg can be read + * from + * @throws IOException Indicated a failure to find the resource + */ + public OggClip(String ref) throws IOException { + try { + init(Thread.currentThread().getContextClassLoader().getResourceAsStream(ref)); + } catch (IOException e) { + throw new IOException("Couldn't find: " + ref); + } + } + + /** + * Create a new clip based on a reference into the class path + * + * @param in The stream from which the ogg can be read from + * @throws IOException Indicated a failure to read from the stream + */ + public OggClip(InputStream in) throws IOException { + init(in); + } + + /** + * Set the default gain value (default volume) + */ + public void setDefaultGain() { + setGain(-1); + } + + /** + * Attempt to set the global gain (volume ish) for the play back. If the + * control is not supported this method has no effect. 1.0 will set maximum + * gain, 0.0 minimum gain + * + * @param gain The gain value + */ + public void setGain(float gain) { + if (gain != -1) { + if ((gain < 0) || (gain > 1)) { + throw new IllegalArgumentException("Volume must be between 0.0 and 1.0"); + } + } + + this.gain = gain; + + if (outputLine == null) { + return; + } + + try { + FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.MASTER_GAIN); + if (gain == -1) { + control.setValue(0); + } else { + float max = control.getMaximum(); + float min = control.getMinimum(); // negative values all seem to be zero? + float range = max - min; + + control.setValue(min + (range * gain)); + } + } catch (IllegalArgumentException e) { + // gain not supported + e.printStackTrace(); + } + } + + /** + * Attempt to set the balance between the two speakers. -1.0 is full left + * speak, 1.0 if full right speaker. Anywhere in between moves between the + * two speakers. If the control is not supported this method has no effect + * + * @param balance The balance value + */ + public void setBalance(float balance) { + this.balance = balance; + + if (outputLine == null) { + return; + } + + try { + FloatControl control = (FloatControl) outputLine.getControl(FloatControl.Type.BALANCE); + control.setValue(balance); + } catch (IllegalArgumentException e) { + // balance not supported + } + } + + /** + * Check the state of the play back + * + * @return True if the playback has been stopped + */ + private boolean checkState() { + while (paused && (player != null)) { + synchronized (player) { + if (player != null) { + try { + player.wait(); + } catch (InterruptedException e) { + // ignored + } + } + } + } + + return stopped(); + } + + /** + * Pause the play back + */ + public void pause() { + paused = true; + oldGain = gain; + setGain(0); + } + + /** + * Check if the stream is paused + * + * @return True if the stream is paused + */ + public boolean isPaused() { + return paused; + } + + /** + * Resume the play back + */ + public void resume() { + if (!paused) { + play(); + return; + } + + paused = false; + + synchronized (player) { + if (player != null) { + player.notify(); + } + } + setGain(oldGain); + } + + /** + * Check if the clip has been stopped + * + * @return True if the clip has been stopped + */ + public boolean stopped() { + return ((player == null) || (!player.isAlive())); + } + + /** + * Initialise the ogg clip + * + * @param in The stream we're going to read from + * @throws IOException Indicates a failure to read from the stream + */ + private void init(InputStream in) throws IOException { + if (in == null) { + throw new IOException("Couldn't find input source"); + } + bitStream = new BufferedInputStream(in); + bitStream.mark(Integer.MAX_VALUE); + } + + /** + * Play the clip once + */ + public void play() { + stop(); + + try { + bitStream.reset(); + } catch (IOException e) { + // ignore if no mark + } + + player = new Thread("OGG Play") { + @Override + public void run() { + try { + playStream(Thread.currentThread()); + } catch (InternalException e) { + e.printStackTrace(); + } + + try { + bitStream.reset(); + } catch (IOException e) { + e.printStackTrace(); + } + }; + }; + player.setDaemon(true); + player.start(); + } + + /** + * Loop the clip - maybe for background music + */ + public void loop() { + stop(); + + try { + bitStream.reset(); + } catch (IOException e) { + // ignore if no mark + } + + player = new Thread("OGG Loop") { + @Override + public void run() { + while (player == Thread.currentThread()) { + try { + playStream(Thread.currentThread()); + } catch (InternalException e) { + e.printStackTrace(); + player = null; + } + + try { + bitStream.reset(); + } catch (IOException e) { + } + } + }; + }; + player.setDaemon(true); + player.start(); + } + + /** + * Stop the clip playing + */ + public void stop() { + if (stopped()) { + return; + } + + player = null; + outputLine.drain(); + } + + /** + * Close the stream being played from + */ + public void close() { + try { + if (bitStream != null) { + bitStream.close(); + } + } catch (IOException e) { + } + } + + /* + * Taken from the JOrbis Player + */ + private void initJavaSound(int channels, int rate) { + try { + AudioFormat audioFormat = new AudioFormat(rate, 16, + channels, true, // PCM_Signed + false // littleEndian + ); + DataLine.Info info = new DataLine.Info(SourceDataLine.class, + audioFormat, AudioSystem.NOT_SPECIFIED); + if (!AudioSystem.isLineSupported(info)) { + throw new Exception("Line " + info + " not supported."); + } + + outputLine = (SourceDataLine) AudioSystem.getLine(info); + // outputLine.addLineListener(this); + outputLine.open(audioFormat); + + this.rate = rate; + this.channels = channels; + + setBalance(balance); + setGain(gain); + } catch (Exception ex) { + LOGGER.log(Level.WARNING, "can not init sound system", ex); + } + } + + /* + * Taken from the JOrbis Player + */ + private SourceDataLine getOutputLine(int channels, int rate) { + if (outputLine == null || this.rate != rate + || this.channels != channels) { + if (outputLine != null) { + outputLine.drain(); + outputLine.stop(); + outputLine.close(); + } + initJavaSound(channels, rate); + outputLine.start(); + } + return outputLine; + } + + /* + * Taken from the JOrbis Player + */ + private void initJOrbis() { + oy = new SyncState(); + os = new StreamState(); + og = new Page(); + op = new Packet(); + + vi = new Info(); + vc = new Comment(); + vd = new DspState(); + vb = new Block(vd); + + buffer = null; + bytes = 0; + + oy.init(); + } + + /* + * Taken from the JOrbis Player + */ + private void playStream(Thread me) throws InternalException { + boolean chained = false; + + initJOrbis(); + + while (true) { + if (checkState()) { + return; + } + + int eos = 0; + + int index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = bitStream.read(buffer, index, BUFSIZE); + } catch (Exception e) { + throw new InternalException(e); + } + oy.wrote(bytes); + + if (chained) { + chained = false; + } else { + if (oy.pageout(og) != 1) { + if (bytes < BUFSIZE) { + break; + } + throw new InternalException("Input does not appear to be an Ogg bitstream."); + } + } + os.init(og.serialno()); + os.reset(); + + vi.init(); + vc.init(); + + if (os.pagein(og) < 0) { + // error; stream version mismatch perhaps + throw new InternalException("Error reading first page of Ogg bitstream data."); + } + + if (os.packetout(op) != 1) { + // no page? must not be vorbis + throw new InternalException("Error reading initial header packet."); + } + + if (vi.synthesis_headerin(vc, op) < 0) { + // error case; not a vorbis header + throw new InternalException("This Ogg bitstream does not contain Vorbis audio data."); + } + + int i = 0; + + while (i < 2) { + while (i < 2) { + if (checkState()) { + return; + } + + int result = oy.pageout(og); + if (result == 0) { + break; // Need more data + } + if (result == 1) { + os.pagein(og); + while (i < 2) { + result = os.packetout(op); + if (result == 0) { + break; + } + if (result == -1) { + throw new InternalException("Corrupt secondary header. Exiting."); + } + vi.synthesis_headerin(vc, op); + i++; + } + } + } + + index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = bitStream.read(buffer, index, BUFSIZE); + } catch (Exception e) { + throw new InternalException(e); + } + if (bytes == 0 && i < 2) { + throw new InternalException("End of file before finding all Vorbis headers!"); + } + oy.wrote(bytes); + } + + convsize = BUFSIZE / vi.channels; + + vd.synthesis_init(vi); + vb.init(vd); + + float[][][] _pcmf = new float[1][][]; + int[] _index = new int[vi.channels]; + + getOutputLine(vi.channels, vi.rate); + + while (eos == 0) { + while (eos == 0) { + if (player != me) { + return; + } + + int result = oy.pageout(og); + if (result == 0) { + break; // need more data + } + if (result == -1) { // missing or corrupt data at this page + // position + // System.err.println("Corrupt or missing data in + // bitstream; + // continuing..."); + } else { + os.pagein(og); + + if (og.granulepos() == 0) { // + chained = true; // + eos = 1; // + break; // + } // + + while (true) { + if (checkState()) { + return; + } + + result = os.packetout(op); + if (result == 0) { + break; // need more data + } + if (result == -1) { // missing or corrupt data at + // this page position + // no reason to complain; already complained + // above + + // System.err.println("no reason to complain; + // already complained above"); + } else { + // we have a packet. Decode it + int samples; + if (vb.synthesis(op) == 0) { // test for + // success! + vd.synthesis_blockin(vb); + } + while ((samples = vd.synthesis_pcmout(_pcmf, + _index)) > 0) { + if (checkState()) { + return; + } + + float[][] pcmf = _pcmf[0]; + int bout = (samples < convsize ? samples + : convsize); + + // convert doubles to 16 bit signed ints + // (host order) and + // interleave + for (i = 0; i < vi.channels; i++) { + int ptr = i * 2; + // int ptr=i; + int mono = _index[i]; + for (int j = 0; j < bout; j++) { + int val = (int) (pcmf[i][mono + j] * 32767.); + if (val > 32767) { + val = 32767; + } + if (val < -32768) { + val = -32768; + } + if (val < 0) { + val = val | 0x8000; + } + convbuffer[ptr] = (byte) (val); + convbuffer[ptr + 1] = (byte) (val >>> 8); + ptr += 2 * (vi.channels); + } + } + outputLine.write(convbuffer, 0, 2 + * vi.channels * bout); + vd.synthesis_read(bout); + } + } + } + if (og.eos() != 0) { + eos = 1; + } + } + } + + if (eos == 0) { + index = oy.buffer(BUFSIZE); + buffer = oy.data; + try { + bytes = bitStream.read(buffer, index, BUFSIZE); + } catch (Exception e) { + throw new InternalException(e); + } + if (bytes == -1) { + break; + } + oy.wrote(bytes); + if (bytes == 0) { + eos = 1; + } + } + } + + os.clear(); + vb.clear(); + vd.clear(); + vi.clear(); + } + + oy.clear(); + } + + private class InternalException extends Exception { + + public InternalException(Exception e) { + super(e); + } + + public InternalException(String msg) { + super(msg); + } + } +} From 600ce6a6ffd6e3df9e56fa28cea0f10200d6260b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 26 Apr 2016 19:41:44 +0200 Subject: [PATCH 236/257] control: use original MIME type of attachment file even after encryption --- src/main/java/org/kontalk/system/AttachmentManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index a41671fb..9c61ea6b 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -161,6 +161,7 @@ private void uploadAsync(OutMessage message) { File file = original = attachment.getFilePath().toFile(); String mime = attachment.getMimeType(); + // maybe resize image for smaller payload if(isImage(mime)) { int maxImgSize = Config.getInstance().getInt(Config.NET_MAX_IMG_SIZE); if (maxImgSize > 0) { @@ -200,7 +201,8 @@ private void uploadAsync(OutMessage message) { if (encryptFile == null) return; file = encryptFile; - mime = ENCRYPT_MIME; + // Note: continue using original MIME type, Android client needs it + //mime = ENCRYPT_MIME; } HTTPFileClient client = this.clientOrNull(); From c3b18d9400550bbbfaeff00aaec24b559c2d6eb1 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Apr 2016 15:09:57 +0200 Subject: [PATCH 237/257] view: improved avatar font (specially for Windows), fixed font metrics (almost) --- .../java/org/kontalk/view/AvatarLoader.java | 17 ++++++++++------- src/main/java/org/kontalk/view/Utils.java | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 7e7b3d96..41d75c35 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -24,6 +24,7 @@ import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; +import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Map; @@ -175,18 +176,20 @@ private static BufferedImage fallback(String text, int colorCode, int size, bool text.substring(0, 1).toUpperCase() : FALLBACK_LETTER; - graphics.setFont(new Font(Font.MONOSPACED, Font.BOLD, size)); + graphics.setFont(new Font(Font.DIALOG, Font.PLAIN, size)); graphics.setColor(LETTER_COLOR); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + FontMetrics fm = graphics.getFontMetrics(); - int w = fm.stringWidth(letter); - int h = fm.getHeight(); - int d = fm.getDescent(); + Rectangle2D r = fm.getStringBounds(letter, graphics); + graphics.drawString(letter, - (size / 2.0f) - (w / 2.0f), - // adjust to font baseline - (size / 2.0f) + (h / 2.0f) - d); + (size - (int) r.getWidth()) / 2.0f, + // adjust to font baseline + // Note: not centered for letters with descent (drawing under + // the baseline), dont know how to get that + (size - (int) r.getHeight()) / 2.0f + fm.getAscent()); return img; } diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index e4492670..93810048 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -141,7 +141,7 @@ static WebTextArea createFingerprintArea() { WebTextArea area = new WebTextArea(); area.setEditable(false); area.setOpaque(false); - area.setFontName(Font.MONOSPACED); + area.setFontName(Font.DIALOG); area.setFontSizeAndStyle(13, true, false); return area; } From 210dc1775b3573de922bf0f1d09092aae40f179f Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Apr 2016 16:51:39 +0200 Subject: [PATCH 238/257] view: rework of avatar image creation --- .../java/org/kontalk/view/AvatarLoader.java | 103 +++++++++--------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/kontalk/view/AvatarLoader.java b/src/main/java/org/kontalk/view/AvatarLoader.java index 41d75c35..e047baf1 100644 --- a/src/main/java/org/kontalk/view/AvatarLoader.java +++ b/src/main/java/org/kontalk/view/AvatarLoader.java @@ -34,6 +34,7 @@ import org.kontalk.model.chat.Chat; import org.kontalk.model.Contact; import org.kontalk.util.MediaUtils; +import org.kontalk.util.Tr; /** * Static functions for loading avatar pictures. @@ -47,13 +48,8 @@ final class AvatarLoader { private static final Color FALLBACK_COLOR = new Color(220, 220, 220); private static final Color GROUP_COLOR = new Color(160, 160, 160); - // TODO i18n? - private static final String FALLBACK_LETTER = "?"; - private static final Map CACHE = new HashMap<>(); - AvatarLoader() {}; - static Image load(Chat chat) { return load(new Item(chat)); } @@ -62,6 +58,12 @@ static Image load(Contact contact) { return load(new Item(contact)); } + static BufferedImage createFallback(int size) { + return fallback(fallbackLetter(), FALLBACK_COLOR, size); + } + + private AvatarLoader() {}; + private static Image load(Item item) { if (!CACHE.containsKey(item)) { CACHE.put(item, item.createImage()); @@ -71,50 +73,62 @@ private static Image load(Item item) { private static class Item { private final Avatar avatar; - private final String label; - private final int colorCode; - private final boolean group; + + private final String letter; + private final Color color; Item(Contact contact) { avatar = contact.getAvatar().orElse(null); - label = contact.getName(); - colorCode = hash(contact.getID()); - group = false; + + if (avatar == null) { + String name = contact.getName(); + letter = labelToLetter(name); + int colorcode = name.isEmpty()? 0 : hash(contact.getID()); + int hue = Math.abs(colorcode) % 360; + color = Color.getHSBColor(hue / 360.0f, 0.8f, 1); + } else { + letter = ""; + color = new Color(0); + } } Item(Chat chat) { - Avatar a = null; - String l = null; - Integer cc = null; - + String l = ""; if (chat.isGroupChat()) { + // nice to have: group picture + avatar = null; // or use number of contacts here? l = chat.getSubject(); - group = true; - // nice to have: group picture + color = GROUP_COLOR; } else { Contact c = chat.getValidContacts().stream().findFirst().orElse(null); if (c != null) { - a = c.getAvatar().orElse(null); - l = c.getName(); - cc = hash(c.getID()); + Item i = new Item(c); + avatar = i.avatar; + l = i.letter; + color = i.color; + } else { + avatar = null; + color = FALLBACK_COLOR; } - group = false; } + letter = labelToLetter(l); + } - avatar = a; - label = l != null ? l : ""; - colorCode = cc != null ? cc : hash(chat.getID()); + private String labelToLetter(String label) { + return label.length() >= 1 ? + label.substring(0, 1).toUpperCase() : + fallbackLetter(); } - Image createImage() { + private Image createImage() { if (avatar != null) { BufferedImage img = avatar.loadImage().orElse(null); if (img != null) return MediaUtils.scaleAsync(img, IMG_SIZE, IMG_SIZE, true); } - return fallback(label, colorCode, IMG_SIZE, group); + return fallback(letter, color, IMG_SIZE); } @Override @@ -127,19 +141,24 @@ public boolean equals(Object o) { Item oItem = (Item) o; return ObjectUtils.equals(avatar, oItem.avatar) && - label.equals(oItem.label) && colorCode == oItem.colorCode && - group == oItem.group; + letter.equals(oItem.letter) && + color.equals(oItem.color); } @Override public int hashCode() { - int hash = 3; - hash = 37 * hash + Objects.hashCode(this.label); - hash = 37 * hash + this.colorCode; + int hash = 7; + hash = 71 * hash + Objects.hashCode(this.avatar); + hash = 71 * hash + Objects.hashCode(this.letter); + hash = 71 * hash + Objects.hashCode(this.color); return hash; } } + private static String fallbackLetter() { + return Tr.tr("?"); + } + // uniform hash // Source: https://stackoverflow.com/a/12996028 private static int hash(int x) { @@ -149,33 +168,13 @@ private static int hash(int x) { return x; } - static BufferedImage createFallback(int size) { - return fallback("", 0, size, false); - } - - private static BufferedImage fallback(String text, int colorCode, int size, boolean group) { + private static BufferedImage fallback(String letter, Color color, int size) { BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); - // color - Color color; - if (group) { - color = GROUP_COLOR; - } else if (!text.isEmpty()) { - int hue = Math.abs(colorCode) % 360; - color = Color.getHSBColor(hue / 360.0f, 0.8f, 1); - } else { - color = FALLBACK_COLOR; - } - Graphics2D graphics = img.createGraphics(); graphics.setColor(color); graphics.fillRect(0, 0, size, size); - // letter - String letter = text.length() >= 1 ? - text.substring(0, 1).toUpperCase() : - FALLBACK_LETTER; - graphics.setFont(new Font(Font.DIALOG, Font.PLAIN, size)); graphics.setColor(LETTER_COLOR); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, From 463496e422c42f946eebaa889bcb0c5a2232b81c Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Apr 2016 17:18:56 +0200 Subject: [PATCH 239/257] model: cleaned up leftovers --- src/main/java/org/kontalk/model/chat/SingleChat.java | 1 - src/main/java/org/kontalk/model/message/MessageContent.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/model/chat/SingleChat.java b/src/main/java/org/kontalk/model/chat/SingleChat.java index 76076d99..75e6dbe6 100644 --- a/src/main/java/org/kontalk/model/chat/SingleChat.java +++ b/src/main/java/org/kontalk/model/chat/SingleChat.java @@ -137,7 +137,6 @@ public boolean equals(Object o) { if (!(o instanceof SingleChat)) return false; SingleChat oChat = (SingleChat) o; - System.out.println(this+" "+oChat); return mMember.equals(oChat.mMember) && mXMPPID.equals(oChat.mXMPPID); } diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index bf1c3404..f4480567 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -365,7 +365,7 @@ void setDownloadProgress(int p) { @Override public String toString() { return "{ATT:url="+mURL+",file="+mFile+",mime="+mMimeType - +"length="+mLength+",status="+mCoderStatus+"}"; + +",length="+mLength+",status="+mCoderStatus+"}"; } // using legacy lib, raw types extend Object From 0949b2d3703630e8642485d3887d9f3c695301d8 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 27 Apr 2016 17:58:36 +0200 Subject: [PATCH 240/257] i18n: (auto)update translation strings --- src/main/resources/i18n/strings.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties index c2263bcf..b0bea7e1 100644 --- a/src/main/resources/i18n/strings.properties +++ b/src/main/resources/i18n/strings.properties @@ -258,3 +258,8 @@ s_5RTB = Reduce size of images before sending s_13FX = Resize image attachments: s_8OBK = Send File s_7YWF = max. size: +s_7NP3 = ? +s_VR5N = XMPP chat ID: +s_UICM = Chat ID: +s_VEBZ = Retry +s_QNW6 = Retry sending message From d3a06a18eff6dfd10b55571b1e63ca3e400d7fa8 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 28 Apr 2016 16:10:15 +0200 Subject: [PATCH 241/257] i18n: deleted languages without translation at all --- src/main/resources/i18n/strings_ar.properties | 0 src/main/resources/i18n/strings_pt.properties | 0 .../i18n/strings_sr@latin.properties | 175 ------------------ 3 files changed, 175 deletions(-) delete mode 100644 src/main/resources/i18n/strings_ar.properties delete mode 100644 src/main/resources/i18n/strings_pt.properties delete mode 100644 src/main/resources/i18n/strings_sr@latin.properties diff --git a/src/main/resources/i18n/strings_ar.properties b/src/main/resources/i18n/strings_ar.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/i18n/strings_pt.properties b/src/main/resources/i18n/strings_pt.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/resources/i18n/strings_sr@latin.properties b/src/main/resources/i18n/strings_sr@latin.properties deleted file mode 100644 index 2333761d..00000000 --- a/src/main/resources/i18n/strings_sr@latin.properties +++ /dev/null @@ -1,175 +0,0 @@ - -# s1 = Options -# s2 = Help -# s_JSQ2 = Send -# s_NMPC = Send Message -# s_8WWE = Quit -# s_6P7W = Connecting... -# s_U0MG = Connected -# s_769Q = Disconnecting... -# s_B56J = Not connected -# s_J1DQ = Connecting failed -# s_EON8 = Connection error -# s_0N2Z = Error -# s_BTPA = Encryption error -# s_CEMO = Decryption error -# s_HTYQ = Unknown error -# s_PD0A = Key for receiver not found. -# s_125B = Unusual coder error -# s_M63K = Unknown error\!? -# s_S39E = Can't load keyfile(s) from archive. -# s_VYCJ = Can't create personal key from key files. -# s_RQHM = Is the public key file valid? -# s_WOYH = Are all key files valid? -# s_53Z5 = Is the passphrase correct? -# s_Q4J7 = Can't change password. Internal error(\!?) -# s_OW1R = Can't write key files to configuration directory. -# s_LHST = Can't read key files from configuration directory. -# s_C9CO = Can't load key files from configuration directory. -# s_6D91 = Please reimport your key. -# s_K94P = Can't create connection -# s_HZPW = Can't connect to server. -# s_QL8R = Is the server address correct? -# s_J8KE = The server rejects the key. -# s_H4GJ = The server does not respond. -# s_PVI0 = Can't login to server. -# s_L1DT = The server rejects the account. Is the specified server correct and the account valid? -# s_X4A9 = Connection to server closed on error. -# s_0QJ9 = The installed Java version is too old -# s_N0DZ = Please install Java 8. -# s_HLM5 = Unsupported Java Version -# s_8FB5 = Can't open key archive. -# s_VTOO = Connect -# s_J8B9 = Connect to Server -# s_2939 = Disconnect -# s_XW05 = Disconnect from Server -# s_RYMX = Set status -# s_66AA = Set status text send to other user -# s_AN83 = Exit -# s_NLZ0 = Exit application -# s_RDGN = Preferences -# s_9UNW = Set application preferences -# s_TKHF = About -# s_4P6T = About Kontalk -# s_X4K8 = New -# s_7MO0 = No threads to display. You can create new threads from your contacts -# s_B8XU = Threads -# s_012T = Add -# s_VA0W = No contacts to display. You have no friends ;( -# s_KBMH = Contacts -# s_6COL = Visit kontalk.org -# s_LPY9 = Notification sound by -# s_M91R = About -# s_NMNS = Status -# s_4QK9 = Your current status\: -# s_ZVBN = Previously used\: -# s_ER26 = Cancel -# s_BIF7 = Save -# s_XK4W = Add New Contact -# s_MIAO = Display Name\: -# s_YTBQ = Encryption -# s_U1UW = Cancel -# s_IBU6 = Save -# s_S8GI = Search... -# s_6RRX = Import Wizard -# s_QXUP = Back -# s_1GYL = Next -# s_WW4U = Finish -# s_UK2S = Success\! -# s_E6EK = Import process finished with\: -# s_M4W3 = Error description\: -# s_44NP = Get Started -# s_VKI8 = Welcome to the import wizard. -# s_TMHY = To use the Kontalk desktop client you need an existing account. -# s_EWJ4 = Please export the key files from your Android device and select them on the next page. -# s_JFS8 = Setup -# s_EKOL = Zip archive containing personal key\: -# s_8HBO = Decryption password for key\: -# s_BWQV = Enter password... -# s_16WW = Show password -# s_GR2F = Zip archive -# s_U6NZ = Import results -# s_RHOQ = Main -# s_XSQM = Account -# s_FVE4 = Privacy -# s_T1AI = Main Settings -# s_WFDO = Connect on startup -# s_GCYS = Show tray icon -# s_V6FV = Close to tray -# s_731N = Enter key sends -# s_6G56 = Enter key sends text, Control+Enter adds new line - or vice versa -# s_4ORM = Custom background\: -# s_H4JO = Account Configuration -# s_BGK0 = Server address\: -# s_6OD2 = Port\: -# s_4WQX = Disable certificate validation -# s_VMP3 = Disable SSL certificate server validation -# s_Y7G0 = Import new Account -# s_9FKT = Save & Connect -# s_X97A = no key loaded -# s_3Q3G = Key fingerprint\: -# s_84MW = Privacy Settings -# s_UP85 = Send chatstate notification -# s_MGHZ = -# s_CYIK = No -# s_9NTE = ? -# s_1SED = Yes -# s_D0BD = YES -# s_3OK6 = Available -# s_G3C7 = Blocked -# s_956V = Last seen -# s_PDYT = New Thread -# s_052Y = Creates a new thread for this contact -# s_OUKY = Edit Contact -# s_AOG4 = Edit this contact -# s_9KDJ = Block Contact -# s_FF9R = Block all messages from this contact -# s_AEX9 = Unblock Contact -# s_QU3S = Unblock this contact -# s_J57E = Delete Contact -# s_AHPF = Delete this contact -# s_8GRW = Edit Contact -# s_45P5 = Encryption Key -# s_AW5R = Available -# s_C0EF = Not Available -# s_X9O1 = Changing the JID is only useful in very rare cases. Are you sure? -# s_HEJA = Please Confirm -# s_ELUO = Edit Thread -# s_4ZBG = Edit this thread -# s_HAK6 = Delete Thread -# s_MD21 = Delete this thread -# s_ZKM0 = Please Confirm -# s_CCNS = no messages yet -# s_FSGV = Last activity -# s_Z35B = -# s_D7B3 = -# s_O85V = Edit Thread -# s_AKL2 = Subject\: -# s_D2D8 = Participants\: -# s_UEN4 = More than one receiver not supported (yet). -# s_LQ9X = Sorry -# s_4LCO = [encrypted] -# s_92KK = ? -# s_OEGV = Attachment\: -# s_BUP5 = Decrypt -# s_7770 = Retry decrypting message -# s_5RDK = Copy -# s_0R7Z = Copy message content -# s_6AKS = unknown -# s_YNOC = not encrypted -# s_7BO5 = encrypted -# s_ZN6V = decrypted -# s_WVG1 = unknown -# s_6OKN = not signed -# s_A812 = signed -# s_CAM4 = verified -# s_4N5N = none -# s_YV5S = Security -# s_QNRV = Problems -# s_IGQC = Permanently delete all messages in this thread? -# s_V5R1 = Custom Background -# s_URN0 = Color\: -# s_X171 = Image\: -# s_T3V1 = loading... -# s_H3ZD = downloading... -# s_44X2 = download failed From bd50789f8beda7285eb962e9a143c9650131ca02 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 28 Apr 2016 14:27:43 +0000 Subject: [PATCH 242/257] Translated using Weblate (German) Currently translated at 100.0% (264 of 264 strings) --- src/main/resources/i18n/strings_de.properties | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index c28523a3..af3cc29d 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -328,3 +328,8 @@ s_7MDX=Anfrage von Online-Status an Kontakt s_OTEU=Automatisch Erlaubnis gewähren s_9S4L=Erlaubnis-Anfrage s_WUDV=Gruppenbesitzer +s_7NP3=? +s_VR5N=XMPP chat ID: +s_UICM=Chat ID: +s_VEBZ=Wiederholen +s_QNW6=Versuche Nachricht erneut zu senden From 20a601c04df805271469135c04a35b8b33e73356 Mon Sep 17 00:00:00 2001 From: Mike Zehbe Date: Thu, 28 Apr 2016 15:58:31 +0000 Subject: [PATCH 243/257] Translated using Weblate (German) Currently translated at 100.0% (264 of 264 strings) --- src/main/resources/i18n/strings_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/strings_de.properties b/src/main/resources/i18n/strings_de.properties index af3cc29d..a12b97ec 100644 --- a/src/main/resources/i18n/strings_de.properties +++ b/src/main/resources/i18n/strings_de.properties @@ -329,7 +329,7 @@ s_OTEU=Automatisch Erlaubnis gewähren s_9S4L=Erlaubnis-Anfrage s_WUDV=Gruppenbesitzer s_7NP3=? -s_VR5N=XMPP chat ID: +s_VR5N=XMPP Chat ID: s_UICM=Chat ID: s_VEBZ=Wiederholen s_QNW6=Versuche Nachricht erneut zu senden From f2b6b7060c725f933db55fed7397bfcb2e8e2165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mladen=20Pejakovi=C4=87?= Date: Thu, 28 Apr 2016 14:54:58 +0000 Subject: [PATCH 244/257] Translated using Weblate (Serbian) Currently translated at 26.8% (71 of 264 strings) --- src/main/resources/i18n/strings_sr.properties | 137 +++++++++++------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/src/main/resources/i18n/strings_sr.properties b/src/main/resources/i18n/strings_sr.properties index 850058fb..5561b418 100644 --- a/src/main/resources/i18n/strings_sr.properties +++ b/src/main/resources/i18n/strings_sr.properties @@ -1,62 +1,61 @@ - -s1 = Опције -s2 = Помоћ -s_JSQ2 = Пошаљи -s_NMPC = Пошаљи поруку -s_8WWE = Напусти -s_6P7W = Повезујем се -s_U0MG = Повезан -s_769Q = Искључујем се... -s_B56J = Неповезан -s_J1DQ = Неуспело повезивање -s_EON8 = Грешка при повезивању -s_0N2Z = Грешка -s_BTPA = Грешка шифровања -s_CEMO = Грешка дешифровања -s_HTYQ = Непозната грешка -s_PD0A = Кључ за примаоца није нађен. -s_125B = Неуобичајена грешка кодера -s_M63K = Непозната грешка\!? -s_S39E = Не могу да учитам фајл кључа из архиве. -s_VYCJ = Не могу да направим лични кључ из фајлова. -s_RQHM = Да ли је јавни кључ важећи? -s_WOYH = Да ли су сви кључеви важећи? -s_53Z5 = Да ли је лозинка исправна? -s_Q4J7 = Не могу да променим лозинку. Интерна грешка(\!?) -s_OW1R = Не могу да упишем фајлове кључа у директоријум подешавања. -s_LHST = Не могу да читам фајлове кључа из директоријума подешавања. -s_C9CO = Не могу да учитам фајлове кључа из директоријума подешавања. -s_6D91 = Поново увезите ваш кључ. -s_K94P = Не могу да направим везу -s_HZPW = Не могу да се повежем са сервером. -s_QL8R = Да ли је адреса сервера тачна? -s_J8KE = Сервер одбија кључ. -s_H4GJ = Сервер не одговара. -s_PVI0 = Не могу да се пријавим. -s_L1DT = Сервер одбија овај налог. Да ли је сервер исправно наведен и налог важећи? -s_X4A9 = Веза је затворена уз грешку. -s_0QJ9 = Инсталирана верзија Јаве је превише стара -s_N0DZ = Инсталирајте Јаву 8 -s_HLM5 = Неподржана верзија Јаве -s_8FB5 = Не могу да отворим архиву кључа. -s_VTOO = Повежи се -s_J8B9 = Повежи се на сервер -s_2939 = Прекини везу -s_XW05 = Прекини везу са сервером -s_RYMX = Постави статус -s_66AA = Поставите поруку стања коју шаљете кориснику -s_AN83 = Изађи -s_NLZ0 = Излази из апликације -s_RDGN = Подешавања -s_9UNW = Подесите апликацију -s_TKHF = О програму -s_4P6T = О програму -s_X4K8 = Ново -s_7MO0 = Нема разговора за приказ. Можете започети нов разговор из контаката +s1=Опције +s2=Помоћ +s_JSQ2=Пошаљи +s_NMPC=Пошаљи поруку +s_8WWE=Напусти +s_6P7W=Повезујем се +s_U0MG=Повезан +s_769Q=Искључујем се... +s_B56J=Неповезан +s_J1DQ=Неуспело повезивање +s_EON8=Грешка при повезивању +s_0N2Z=Грешка +s_BTPA=Грешка шифровања +s_CEMO=Грешка дешифровања +s_HTYQ=Непозната грешка +s_PD0A=Кључ за примаоца није нађен. +s_125B=Неуобичајена грешка кодера +s_M63K=Непозната грешка!? +s_S39E=Не могу да учитам фајл кључа из архиве. +s_VYCJ=Не могу да направим лични кључ из фајлова. +s_RQHM=Да ли је јавни кључ важећи? +s_WOYH=Да ли су сви кључеви важећи? +s_53Z5=Да ли је лозинка исправна? +s_Q4J7=Не могу да променим лозинку. Интерна грешка(!?) +s_OW1R=Не могу да упишем фајлове кључа у директоријум подешавања. +s_LHST=Не могу да читам фајлове кључа из директоријума подешавања. +s_C9CO=Не могу да учитам фајлове кључа из директоријума подешавања. +s_6D91=Поново увезите ваш кључ. +s_K94P=Не могу да направим везу +s_HZPW=Не могу да се повежем са сервером. +s_QL8R=Да ли је адреса сервера тачна? +s_J8KE=Сервер одбија кључ. +s_H4GJ=Сервер не одговара. +s_PVI0=Не могу да се пријавим. +s_L1DT=Сервер одбија овај налог. Да ли је сервер исправно наведен и налог важећи? +s_X4A9=Веза је затворена уз грешку. +s_0QJ9=Инсталирана верзија Јаве је превише стара +s_N0DZ=Инсталирајте Јаву 8. +s_HLM5=Неподржана верзија Јаве +s_8FB5=Не могу да отворим архиву кључа. +s_VTOO=Повежи се +s_J8B9=Повежи се на сервер +s_2939=Прекини везу +s_XW05=Прекини везу са сервером +s_RYMX=Постави статус +s_66AA=Поставите поруку стања коју шаљете кориснику +s_AN83=Изађи +s_NLZ0=Излази из апликације +s_RDGN=Подешавања +s_9UNW=Подесите апликацију +s_TKHF=О програму +s_4P6T=О програму +s_X4K8=Ново +s_7MO0=Нема разговора за приказ. Можете започети нов разговор из контаката # s_B8XU = Threads # s_012T = Add # s_VA0W = No contacts to display. You have no friends ;( -s_KBMH = Контакти +s_KBMH=Контакти # s_6COL = Visit kontalk.org # s_LPY9 = Notification sound by # s_M91R = About @@ -173,3 +172,29 @@ s_KBMH = Контакти # s_T3V1 = loading... # s_H3ZD = downloading... # s_44X2 = download failed + +s_6COL=Посетите kontalk.org +s_NMNS=Стање +s_4QK9=Ваше текуће стање: +s_ZVBN=Претходно кориштено: +s_ER26=Одустани +s_BIF7=Сачувај +s_YTBQ=Шифровање +s_QXUP=Назад +s_1GYL=Следеће +s_UK2S=Успех! +s_M4W3=Опис грешке: +s_44NP=Почнимо +s_16WW=Прикажи лозинку +s_XSQM=Налог +s_RHOQ=Главно +s_FVE4=Приватност +s_T1AI=Главне поставке +s_BGK0=Адреса сервера: +s_6OD2=Порт: +s_84MW=Поставке приватности +s_URN0=Боја: +s_X171=Слика: +s_44X2=преузимање није успело +s_WDMP=Отисак: +s_HKW8=Завршено From a9418f7f41e0e63dd547d106e8664bcb66fb4775 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Sat, 30 Apr 2016 18:01:07 +0200 Subject: [PATCH 245/257] view: show new chat after creation (got lost somewhere) --- src/main/java/org/kontalk/view/ContactListView.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 457fb68b..02f227c6 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -36,6 +36,7 @@ import org.apache.commons.lang.StringEscapeUtils; import org.kontalk.model.Contact; import org.kontalk.model.Model; +import org.kontalk.model.chat.Chat; import org.kontalk.system.Control; import org.kontalk.util.Tr; import org.kontalk.view.ContactListView.ContactItem; @@ -92,8 +93,9 @@ protected WebPopupMenu rightClickMenu(ContactItem item) { newItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { - mView.getControl().getOrCreateSingleChat( + Chat chat = mView.getControl().getOrCreateSingleChat( ContactListView.this.getSelectedItem().mValue); + mView.showChat(chat); } }); menu.add(newItem); From 2310263bd6c9991c0ebf396930c654d72c5ed42d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 2 May 2016 16:17:33 +0200 Subject: [PATCH 246/257] model: create user contact when setting user JID --- src/main/java/org/kontalk/misc/JID.java | 2 +- src/main/java/org/kontalk/model/Account.java | 10 -------- src/main/java/org/kontalk/model/Contact.java | 2 +- .../java/org/kontalk/model/ContactList.java | 11 +++------ src/main/java/org/kontalk/model/Model.java | 24 +++++++++++++++---- .../kontalk/model/message/MessageContent.java | 4 ++-- src/main/java/org/kontalk/system/Control.java | 2 +- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/kontalk/misc/JID.java b/src/main/java/org/kontalk/misc/JID.java index 9d1e88c2..3500724b 100644 --- a/src/main/java/org/kontalk/misc/JID.java +++ b/src/main/java/org/kontalk/misc/JID.java @@ -87,7 +87,7 @@ public JID toBare() { /** * Comparing only bare JIDs. - * Case-insensitive (local and domain part, resource is case-sensitive). + * Case-insensitive. */ @Override public boolean equals(Object o) { diff --git a/src/main/java/org/kontalk/model/Account.java b/src/main/java/org/kontalk/model/Account.java index 8233f483..0b84db5d 100644 --- a/src/main/java/org/kontalk/model/Account.java +++ b/src/main/java/org/kontalk/model/Account.java @@ -38,7 +38,6 @@ import org.kontalk.crypto.PGPUtils; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.X509Bridge; -import org.kontalk.misc.JID; import org.kontalk.persistence.Config; import org.kontalk.util.EncodingUtils; @@ -126,10 +125,6 @@ public void setPassword(char[] oldPassword, char[] newPassword) throws KonExcept this.writePrivateKey(privateKeyData, oldPassword, newPassword); } - public void setJID(JID jid) { - Config.getInstance().setProperty(Config.ACC_JID, jid.string()); - } - private void writePrivateKey(byte[] privateKeyData, char[] oldPassword, char[] newPassword) @@ -195,9 +190,4 @@ private void writeBytesToFile(byte[] bytes, String filename, boolean armored) th private boolean fileExists(String filename) { return new File(mKeyDir.toString(), filename).isFile(); } - - // TODO - public static JID getUserJID() { - return JID.bare(Config.getInstance().getString(Config.ACC_JID)); - } } diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index d631f573..3dee6f33 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -298,7 +298,7 @@ public void deleteAvatar() { } public boolean isMe() { - return mJID.isValid() && mJID.equals(Account.getUserJID()); + return mJID.isValid() && mJID.equals(Model.getUserJID()); } public boolean isKontalkUser(){ diff --git a/src/main/java/org/kontalk/model/ContactList.java b/src/main/java/org/kontalk/model/ContactList.java index 108305c7..fa453446 100644 --- a/src/main/java/org/kontalk/model/ContactList.java +++ b/src/main/java/org/kontalk/model/ContactList.java @@ -103,19 +103,14 @@ public Optional get(JID jid) { } /** - * Get the contact that represents the user itself. It is created and added - * if not yet in the list. + * Get the contact that represents the user itself. */ public Optional getMe() { - JID myJID = Account.getUserJID(); + JID myJID = Model.getUserJID(); if (!myJID.isValid()) return Optional.empty(); - Contact me = this.get(myJID).orElse(null); - if (me != null) - return Optional.of(me); - - return this.create(myJID, ""); + return this.get(myJID); } public Set getAll(boolean withMe) { diff --git a/src/main/java/org/kontalk/model/Model.java b/src/main/java/org/kontalk/model/Model.java index 253d904c..de0bfdd4 100644 --- a/src/main/java/org/kontalk/model/Model.java +++ b/src/main/java/org/kontalk/model/Model.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Optional; import java.util.logging.Logger; +import org.kontalk.misc.JID; import org.kontalk.model.Avatar.UserAvatar; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.ChatList; @@ -101,6 +102,20 @@ public UserAvatar setUserAvatar(BufferedImage image) { return UserAvatar.create(image); } + public void deleteUserAvatar() { + mUserAvatar.delete(); + mUserAvatar = new UserAvatar(APP_DIR); + } + + public void setUserJID(JID jid) { + Config.getInstance().setProperty(Config.ACC_JID, jid.string()); + + if (!mContactList.contains(jid)) { + LOGGER.info("creating user contact, jid: "+jid); + mContactList.create(jid, ""); + } + } + public Optional createInMessage(ProtoMessage protoMessage, Chat chat, ClientUtils.MessageIDs ids, Optional serverDate) { InMessage newMessage = new InMessage(protoMessage, chat, ids.jid, @@ -134,11 +149,6 @@ public Optional createOutMessage(Chat chat, return Optional.of(newMessage); } - public void deleteUserAvatar() { - mUserAvatar.delete(); - mUserAvatar = new UserAvatar(APP_DIR); - } - static Path appDir() { if (APP_DIR == null) throw new IllegalStateException("model not set up"); @@ -152,4 +162,8 @@ public static Database database(){ return DATABASE; } + + public static JID getUserJID() { + return JID.bare(Config.getInstance().getString(Config.ACC_JID)); + } } diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index f4480567..e36550bf 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -33,7 +33,7 @@ import org.json.simple.JSONObject; import org.json.simple.JSONValue; import org.kontalk.crypto.Coder; -import org.kontalk.model.Account; +import org.kontalk.model.Model; import org.kontalk.model.chat.GroupMetaData.KonGroupData; import org.kontalk.util.EncodingUtils; @@ -543,7 +543,7 @@ public List getAdded() { } public boolean isAddingMe() { - JID myJID = Account.getUserJID(); + JID myJID = Model.getUserJID(); return mAdded.stream().anyMatch(jid -> jid.equals(myJID)); } diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index 1d520f8d..e44b6c35 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -208,7 +208,7 @@ public void onStatusChange(Status status, EnumSet feat } public void onAuthenticated(JID jid) { - mModel.account().setJID(jid); + mModel.setUserJID(jid); } public void onException(KonException ex) { From d770dcabc9f87e2edf3244aec86741efb0de3469 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 2 May 2016 16:19:17 +0200 Subject: [PATCH 247/257] build: setting ext main class not needed anymore (?) for testing --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 554202cd..833b8279 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,6 @@ sourceCompatibility = '1.8' targetCompatibility = '1.8' mainClassName = 'org.kontalk.Kontalk' -ext.mainClass = mainClassName ext.clientCommonDir = 'client-common-java' applicationDefaultJvmArgs = ['''-Djava.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS|%4$-6s|%2$s-%5$s%6$s%n'''] From 9fbba36c6cf86e722c16f2e5e39b64f9376a4aee Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 2 May 2016 16:20:34 +0200 Subject: [PATCH 248/257] view: EDT thread safety --- src/main/java/org/kontalk/view/ListView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/kontalk/view/ListView.java b/src/main/java/org/kontalk/view/ListView.java index b9357078..b6b0eb2b 100644 --- a/src/main/java/org/kontalk/view/ListView.java +++ b/src/main/java/org/kontalk/view/ListView.java @@ -314,7 +314,7 @@ void filterItems(String search) { private void timerUpdate() { for (int i = 0; i < mModel.getRowCount(); i++) { I item = (I) mModel.getValueAt(i, 0); - item.update(null, mTimer); + item.updateOnEDT(mTimer); } } From 5f9634f6460b1e70ed6ccf80c515fc21bebd7f9b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 2 May 2016 18:24:52 +0200 Subject: [PATCH 249/257] view: can't show deleted users --- .../java/org/kontalk/view/ChatListView.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index baa3d2f2..32779427 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -106,15 +106,17 @@ protected WebPopupMenu rightClickMenu(ChatItem item) { Chat chat = item.mValue; if (chat instanceof SingleChat) { final Contact contact = ((SingleChat) chat).getContact(); - WebMenuItem editItem = new WebMenuItem(Tr.tr("Edit Contact")); - editItem.setToolTipText(Tr.tr("Edit contact settings")); - editItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent event) { - mView.showContactDetails(contact); - } - }); - menu.add(editItem); + if (!contact.isDeleted()) { + WebMenuItem editItem = new WebMenuItem(Tr.tr("Edit Contact")); + editItem.setToolTipText(Tr.tr("Edit contact settings")); + editItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + mView.showContactDetails(contact); + } + }); + menu.add(editItem); + } } WebMenuItem deleteItem = new WebMenuItem(Tr.tr("Delete Chat")); From 3cdb3e5c272b21c5b955628c4cb6e73b32deeaa0 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Mon, 2 May 2016 18:59:26 +0200 Subject: [PATCH 250/257] view: contact list item: reworked update --- .../org/kontalk/view/ContactListView.java | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 02f227c6..6e7d1a51 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -50,7 +50,7 @@ final class ContactListView extends ListView implements Ob private final Model mModel; ContactListView(final View view, Model model) { - super(view, true); + super(view, false); mModel = model; @@ -239,27 +239,35 @@ protected boolean contains(String search) { @Override protected void updateOnEDT(Object arg) { - // avatar - mAvatar.setImage(AvatarLoader.load(mValue)); - - // name - String name = Utils.displayName(mValue); - if (!name.equals(mNameLabel.getText())) { - mNameLabel.setText(name); - ContactListView.this.updateSorting(); + if (arg == null || arg instanceof String) { + // avatar + // TODO update not always working, 'A'-> yes, 'z'-> no; wtf? + mAvatar.setImage(AvatarLoader.load(mValue)); + + // name + String name = Utils.displayName(mValue); + if (!name.equals(mNameLabel.getText())) { + mNameLabel.setText(name); + ContactListView.this.updateSorting(); + } } // status - mStatusLabel.setText(Utils.mainStatus(mValue, false)); + if (arg == null || arg instanceof Contact.Subscription || + arg instanceof Contact.Online) { + mStatusLabel.setText(Utils.mainStatus(mValue, false)); + } // online status - Contact.Subscription subStatus = mValue.getSubScription(); - mBackground = mValue.getOnline() == Contact.Online.YES ? View.LIGHT_BLUE: - subStatus == Contact.Subscription.UNSUBSCRIBED || - subStatus == Contact.Subscription.PENDING || - mValue.isBlocked() ? View.LIGHT_GREY : - Color.WHITE; - this.setBackground(mBackground); + if (arg == null || arg instanceof Contact.Subscription) { + Contact.Subscription subStatus = mValue.getSubScription(); + mBackground = mValue.getOnline() == Contact.Online.YES ? View.LIGHT_BLUE: + subStatus == Contact.Subscription.UNSUBSCRIBED || + subStatus == Contact.Subscription.PENDING || + mValue.isBlocked() ? View.LIGHT_GREY : + Color.WHITE; + this.setBackground(mBackground); + } ContactListView.this.repaint(); } From 3e33f3becb7eeb815787d1e86bb1074ec0f8336b Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 3 May 2016 18:07:57 +0200 Subject: [PATCH 251/257] view: fixing bug in WebLaf's WebImage class when setting new avatar image And took me only some hours to find this --- src/main/java/org/kontalk/view/ChatListView.java | 3 +-- src/main/java/org/kontalk/view/ContactListView.java | 3 +-- src/main/java/org/kontalk/view/Utils.java | 11 +++++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/kontalk/view/ChatListView.java b/src/main/java/org/kontalk/view/ChatListView.java index 32779427..511c689f 100644 --- a/src/main/java/org/kontalk/view/ChatListView.java +++ b/src/main/java/org/kontalk/view/ChatListView.java @@ -222,7 +222,7 @@ private void updateView(Object arg) { // avatar may change when subject or contact name changes if (arg == null || arg instanceof Contact || arg instanceof String) { - mAvatar.setImage(AvatarLoader.load(mValue)); + Utils.fixedSetWebImageImage(mAvatar, AvatarLoader.load(mValue)); } if (arg == null || arg instanceof KonMessage) { @@ -253,7 +253,6 @@ private void updateView(Object arg) { mChatStateLabel.setText(stateText); mStatusLabel.setVisible(false); } - } private void updateBG() { diff --git a/src/main/java/org/kontalk/view/ContactListView.java b/src/main/java/org/kontalk/view/ContactListView.java index 6e7d1a51..ede27261 100644 --- a/src/main/java/org/kontalk/view/ContactListView.java +++ b/src/main/java/org/kontalk/view/ContactListView.java @@ -241,8 +241,7 @@ protected boolean contains(String search) { protected void updateOnEDT(Object arg) { if (arg == null || arg instanceof String) { // avatar - // TODO update not always working, 'A'-> yes, 'z'-> no; wtf? - mAvatar.setImage(AvatarLoader.load(mValue)); + Utils.fixedSetWebImageImage(mAvatar, AvatarLoader.load(mValue)); // name String name = Utils.displayName(mValue); diff --git a/src/main/java/org/kontalk/view/Utils.java b/src/main/java/org/kontalk/view/Utils.java index 93810048..ed36385f 100644 --- a/src/main/java/org/kontalk/view/Utils.java +++ b/src/main/java/org/kontalk/view/Utils.java @@ -19,6 +19,7 @@ package org.kontalk.view; import com.alee.extended.filechooser.WebFileChooserField; +import com.alee.extended.image.WebImage; import com.alee.laf.menu.WebMenuItem; import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.optionpane.WebOptionPane; @@ -175,6 +176,16 @@ static Image getImage(String fileName) { return Toolkit.getDefaultToolkit().createImage(imageUrl); } + static void fixedSetWebImageImage(WebImage webImage, Image img) { + // works cause caching + if (!img.equals(webImage.getImage())) { + // + webImage.setEnabled(false); + webImage.setImage(img); + webImage.setEnabled(true); + } + } + /* strings */ static String name(Contact contact, int maxLength) { From fafef78b597f08d51de801670951d86b6c44f382 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Tue, 3 May 2016 19:57:57 +0200 Subject: [PATCH 252/257] control: send key request whenever needed --- src/main/java/org/kontalk/model/Contact.java | 2 +- src/main/java/org/kontalk/system/Control.java | 30 +++++++++++-------- .../org/kontalk/system/RosterHandler.java | 18 +++++++---- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/kontalk/model/Contact.java b/src/main/java/org/kontalk/model/Contact.java index 3dee6f33..7480ee17 100644 --- a/src/main/java/org/kontalk/model/Contact.java +++ b/src/main/java/org/kontalk/model/Contact.java @@ -262,7 +262,7 @@ public Subscription getSubScription() { return mSubStatus; } - public void setSubScriptionStatus(Subscription status) { + public void setSubscriptionStatus(Subscription status) { if (status == mSubStatus) return; diff --git a/src/main/java/org/kontalk/system/Control.java b/src/main/java/org/kontalk/system/Control.java index e44b6c35..1bbed8a4 100644 --- a/src/main/java/org/kontalk/system/Control.java +++ b/src/main/java/org/kontalk/system/Control.java @@ -222,7 +222,7 @@ public void onEncryptionErrors(KonMessage message, Contact contact) { errors.contains(Coder.Error.INVALID_SIGNATURE) || errors.contains(Coder.Error.INVALID_SENDER)) { // maybe there is something wrong with the senders key - this.maySendKeyRequest(contact); + this.sendKeyRequest(contact); } this.onSecurityErrors(message); } @@ -361,7 +361,7 @@ void onPGPKey(Contact contact, byte[] rawKey) { // ask before overwriting mViewControl.changed(new ViewEvent.NewKey(contact, key)); } else { - this.setKey(contact, key); + setKey(contact, key); } } @@ -456,19 +456,25 @@ boolean sendMessage(OutMessage message) { return sent; } + private static boolean canSendKeyRequest(Contact contact) { + return contact.isMe() || + (contact.isKontalkUser() && + contact.getSubScription() == Contact.Subscription.SUBSCRIBED); + } + void maySendKeyRequest(Contact contact) { - if (!contact.isKontalkUser()) { - LOGGER.config("not sending, not a kontalk user, contact: "+contact); - return; - } + if (canSendKeyRequest(contact)) + this.sendKeyRequest(contact); + } - if (contact.getSubScription() != Contact.Subscription.SUBSCRIBED) { - LOGGER.config("not sending, no subscription, contact: "+contact); + void sendKeyRequest(Contact contact) { + if (!canSendKeyRequest(contact)) { + LOGGER.warning("better do not, contact: "+contact); return; } + mClient.sendPublicKeyRequest(contact.getJID()); } - Optional getOrCreateContact(JID jid) { Contact contact = mModel.contacts().get(jid).orElse(null); if (contact != null) @@ -531,7 +537,7 @@ private void decryptAndProcess(InMessage message) { this.processContent(message); } - private void setKey(Contact contact, PGPCoderKey key) { + private static void setKey(Contact contact, PGPCoderKey key) { contact.setKey(key.rawKey, key.fingerprint); // enable encryption without asking @@ -705,11 +711,11 @@ public void changeName(Contact contact, String name) { } public void requestKey(Contact contact) { - Control.this.maySendKeyRequest(contact); + Control.this.sendKeyRequest(contact); } public void acceptKey(Contact contact, PGPCoderKey key) { - Control.this.setKey(contact, key); + setKey(contact, key); } public void declineKey(Contact contact) { diff --git a/src/main/java/org/kontalk/system/RosterHandler.java b/src/main/java/org/kontalk/system/RosterHandler.java index f9c5d564..f85f2a8c 100644 --- a/src/main/java/org/kontalk/system/RosterHandler.java +++ b/src/main/java/org/kontalk/system/RosterHandler.java @@ -32,6 +32,7 @@ import org.kontalk.misc.ViewEvent; import org.kontalk.model.Contact; import org.kontalk.misc.JID; +import org.kontalk.model.Contact.Subscription; import org.kontalk.model.Model; /** @@ -83,7 +84,9 @@ public void onEntryAdded(JID jid, return; Contact.Subscription status = rosterToModelSubscription(itemStatus, type); - newContact.setSubScriptionStatus(status); + newContact.setSubscriptionStatus(status); + + mControl.maySendKeyRequest(newContact); if (status == Contact.Subscription.UNSUBSCRIBED) mControl.sendPresenceSubscription(jid, Client.PresenceCommand.REQUEST); @@ -110,7 +113,10 @@ public void onEntryUpdate(JID jid, return; } // subcription may have changed - contact.setSubScriptionStatus(rosterToModelSubscription(itemStatus, type)); + contact.setSubscriptionStatus(rosterToModelSubscription(itemStatus, type)); + + // maybe subscribed now + mControl.maySendKeyRequest(contact); // name may have changed if (contact.getName().isEmpty() && !name.equals(jid.local())) @@ -239,16 +245,16 @@ private boolean isMe(JID jid) { return myJID != null ? myJID.equals(jid) : false; } - private static Contact.Subscription rosterToModelSubscription( + private static Subscription rosterToModelSubscription( RosterPacket.ItemStatus status, RosterPacket.ItemType type) { if (type == RosterPacket.ItemType.both || type == RosterPacket.ItemType.to || type == RosterPacket.ItemType.remove) - return Contact.Subscription.SUBSCRIBED; + return Subscription.SUBSCRIBED; if (status == RosterPacket.ItemStatus.SUBSCRIPTION_PENDING) - return Contact.Subscription.PENDING; + return Subscription.PENDING; - return Contact.Subscription.UNSUBSCRIBED; + return Subscription.UNSUBSCRIBED; } } From cab4b864f4f3f63376cbbab685fb5cd36aebc30d Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 4 May 2016 23:00:34 +0200 Subject: [PATCH 253/257] system: do not use mime type field in model for incoming attachment files --- .../kontalk/model/message/MessageContent.java | 32 +++++++++---------- .../org/kontalk/system/AttachmentManager.java | 21 +++--------- .../java/org/kontalk/util/ClientUtils.java | 3 +- .../java/org/kontalk/util/MediaUtils.java | 15 +++++++-- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/kontalk/model/message/MessageContent.java b/src/main/java/org/kontalk/model/message/MessageContent.java index e36550bf..af2efb00 100644 --- a/src/main/java/org/kontalk/model/message/MessageContent.java +++ b/src/main/java/org/kontalk/model/message/MessageContent.java @@ -268,7 +268,7 @@ public static class Attachment { private URI mURL; // file name of downloaded file or path to upload file, empty by default private Path mFile; - // MIME of file, empty string by default + // MIME of file, only used for outgoing, empty string by default private String mMimeType; // size of (decrypted) upload file in bytes, -1 by default private long mLength; @@ -277,31 +277,31 @@ public static class Attachment { // progress downloaded of (encrypted) file in percent private int mDownloadProgress = -1; - // used for outgoing attachments - public Attachment(Path path, String mimeType) { - this(URI.create(""), path, mimeType, -1, - CoderStatus.createInsecure()); + // used when loading from database. + private Attachment(URI url, Path file, + String mimeType, long length, + CoderStatus coderStatus) { + mURL = url; + mFile = file; + mMimeType = mimeType; + mLength = length; + mCoderStatus = coderStatus; } // used for incoming attachments - public Attachment(URI url, String mimeType, long length, + public static Attachment incoming(URI url, long length, boolean encrypted) { - this(url, Paths.get(""), mimeType, length, + return new Attachment(url, Paths.get(""), "", length, encrypted ? CoderStatus.createEncrypted() : CoderStatus.createInsecure() ); } - // used when loading from database. - private Attachment(URI url, Path file, - String mimeType, long length, - CoderStatus coderStatus) { - mURL = url; - mFile = file; - mMimeType = mimeType; - mLength = length; - mCoderStatus = coderStatus; + // used for outgoing attachments + public static Attachment outgoing(Path path, String mimeType) { + return new Attachment(URI.create(""), path, mimeType, -1, + CoderStatus.createInsecure()); } public boolean hasURL() { diff --git a/src/main/java/org/kontalk/system/AttachmentManager.java b/src/main/java/org/kontalk/system/AttachmentManager.java index 9c61ea6b..0e98c73d 100644 --- a/src/main/java/org/kontalk/system/AttachmentManager.java +++ b/src/main/java/org/kontalk/system/AttachmentManager.java @@ -308,10 +308,9 @@ boolean mayCreateImagePreview(KonMessage message) { } Path path = absoluteFilePath(att); - String mime = att.getMimeType(); - if (mime.isEmpty()) - // guess from file - mime = mimeForFile(path); + String mime = StringUtils.defaultIfEmpty(att.getMimeType(), + // guess from file + MediaUtils.mimeForFile(path)); if (!isImage(mime)) return false; @@ -413,23 +412,13 @@ static Attachment createAttachmentOrNull(Path path) { return null; } - String mimeType = mimeForFile(path); + String mimeType = MediaUtils.mimeForFile(path); if (mimeType.isEmpty()) { LOGGER.warning("no mime type for file: "+path); return null; } - return new Attachment(path, mimeType); - } - - private static String mimeForFile(Path path) { - String mime = null; - try { - mime = Files.probeContentType(path); - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "can't probe type", ex); - } - return StringUtils.defaultString(mime); + return Attachment.outgoing(path, mimeType); } private static boolean isImage(String mimeType) { diff --git a/src/main/java/org/kontalk/util/ClientUtils.java b/src/main/java/org/kontalk/util/ClientUtils.java index 361de399..86f9d082 100644 --- a/src/main/java/org/kontalk/util/ClientUtils.java +++ b/src/main/java/org/kontalk/util/ClientUtils.java @@ -137,8 +137,7 @@ public static MessageContent parseMessageContent(Message m) { LOGGER.log(Level.WARNING, "can't parse URL", ex); url = URI.create(""); } - attachment = new MessageContent.Attachment(url, - oobData.getMime() != null ? oobData.getMime() : "", + attachment = MessageContent.Attachment.incoming(url, oobData.getLength(), oobData.isEncrypted()); diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 330ff03f..483558f7 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -26,6 +26,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Level; @@ -44,7 +45,7 @@ public class MediaUtils { private static final Logger LOGGER = Logger.getLogger(MediaUtils.class.getName()); - private static OggClip mAudioClip = null; + private MediaUtils() {} public static String extensionForMIME(String mimeType) { MimeType mime = null; @@ -61,9 +62,19 @@ public static String extensionForMIME(String mimeType) { return StringUtils.defaultIfEmpty(m, "dat"); } + public static String mimeForFile(Path path) { + String mime = null; + try { + mime = Files.probeContentType(path); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't probe type", ex); + } + return StringUtils.defaultString(mime); + } + public enum Sound{NOTIFICATION} - private MediaUtils() {} + private static OggClip mAudioClip = null; public static void playSound(Sound sound) { switch (sound) { From cbf8b76e9121143edbfc8442acf68d81e90cbe66 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Wed, 4 May 2016 23:03:23 +0200 Subject: [PATCH 254/257] crypto: get extension for decrypted file from file content --- .../java/org/kontalk/crypto/Decryptor.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/kontalk/crypto/Decryptor.java b/src/main/java/org/kontalk/crypto/Decryptor.java index 65a234a6..f7aefbc4 100644 --- a/src/main/java/org/kontalk/crypto/Decryptor.java +++ b/src/main/java/org/kontalk/crypto/Decryptor.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.text.ParseException; import java.util.Arrays; @@ -52,12 +53,12 @@ import org.bouncycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.packet.Message; -import org.kontalk.client.KonMessageListener; import org.kontalk.model.message.MessageContent; import org.kontalk.model.message.DecryptMessage; import org.kontalk.model.message.InMessage; import org.kontalk.util.CPIMMessage; import org.kontalk.util.ClientUtils; +import org.kontalk.util.MediaUtils; import org.kontalk.util.XMPPUtils; import org.xmlpull.v1.XmlPullParserException; @@ -161,10 +162,9 @@ void decryptAttachment(Path baseDir) { File inFile = baseDir.resolve(attachment.getFilePath()).toFile(); // out file String base = FilenameUtils.getBaseName(inFile.getName()); - String ext = FilenameUtils.getExtension(inFile.getName()); - File outFile = baseDir.resolve(base + "_dec." + ext).toFile(); + File outFile = baseDir.resolve(base + "_dec").toFile(); if (outFile.exists()) { - LOGGER.warning("encrypted file already exists: "+outFile.getAbsolutePath()); + LOGGER.warning("decrypted file already exists: "+outFile.getAbsolutePath()); return; } @@ -186,8 +186,17 @@ void decryptAttachment(Path baseDir) { inMessage.setAttachmentSigning(decResult.signing); // set new filename - inMessage.setDecryptedAttachment(outFile.getName()); - LOGGER.info("attachment decryption successful"); + Path outPath = outFile.toPath(); + Path newPath = outPath.resolveSibling(outFile.getName() + "." + + MediaUtils.extensionForMIME(MediaUtils.mimeForFile(outPath))); + try { + outPath = Files.move(outFile.toPath(), newPath); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't rename file", ex); + } + + inMessage.setDecryptedAttachment(outPath.toFile().getName()); + LOGGER.info("success, decrypted file: "+outPath); } /** Decrypt, verify and write input stream data to output stream. */ From da930143a0b950592b53a224578845a2908a0242 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 5 May 2016 17:25:35 +0200 Subject: [PATCH 255/257] utils: file MIME type detection for buggy Windows --- .../java/org/kontalk/util/MediaUtils.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/main/java/org/kontalk/util/MediaUtils.java b/src/main/java/org/kontalk/util/MediaUtils.java index 483558f7..5726797c 100644 --- a/src/main/java/org/kontalk/util/MediaUtils.java +++ b/src/main/java/org/kontalk/util/MediaUtils.java @@ -22,10 +22,14 @@ import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; @@ -48,6 +52,9 @@ public class MediaUtils { private MediaUtils() {} public static String extensionForMIME(String mimeType) { + if (mimeType.isEmpty()) + return "unk"; + MimeType mime = null; try { mime = MimeTypes.getDefaultMimeTypes().forName(mimeType); @@ -69,6 +76,20 @@ public static String mimeForFile(Path path) { } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't probe type", ex); } + + if (mime == null) { + // method above is buggy on windows, try something else + try(FileInputStream fis = new FileInputStream(path.toFile())) { + InputStream is = new BufferedInputStream(fis); + mime = URLConnection.guessContentTypeFromStream(is); + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "can't guess content type", ex); + } + } + + if (mime == null) + LOGGER.warning("can't determine content type: "+path); + return StringUtils.defaultString(mime); } From 8c682163481dd220d263b8a78be27a0b8fb79942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Suso=20Comesa=C3=B1a?= Date: Wed, 4 May 2016 07:09:15 +0000 Subject: [PATCH 256/257] Translated using Weblate (Spanish) Currently translated at 99.2% (262 of 264 strings) --- src/main/resources/i18n/strings_es.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/i18n/strings_es.properties b/src/main/resources/i18n/strings_es.properties index 7f8354db..37780155 100644 --- a/src/main/resources/i18n/strings_es.properties +++ b/src/main/resources/i18n/strings_es.properties @@ -332,3 +332,6 @@ s_5RTB=Reducir el tamaño de las imágenes antes de enviar s_13FX=Cambiar el tamaño de las imágenes adjuntas: s_8OBK=Enviar archivo s_7YWF=max. tamaño: +s_7NP3=? +s_VR5N=Indentificación del chat XMPP +s_UICM=Indentificación del Chat: From 59ea5099a065ec10830cfc8cee1c8cb2df0ac8e1 Mon Sep 17 00:00:00 2001 From: Alexander Bikadorov Date: Thu, 5 May 2016 17:37:07 +0200 Subject: [PATCH 257/257] next release version: 3.1 --- src/main/java/org/kontalk/Kontalk.java | 2 +- win_installer/create_installer.nsi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/kontalk/Kontalk.java b/src/main/java/org/kontalk/Kontalk.java index afbae0bb..acd76f73 100644 --- a/src/main/java/org/kontalk/Kontalk.java +++ b/src/main/java/org/kontalk/Kontalk.java @@ -52,7 +52,7 @@ public final class Kontalk { private static final Logger LOGGER = Logger.getLogger(Kontalk.class.getName()); - public static final String VERSION = "3.0.4"; + public static final String VERSION = "3.1"; private final Path mAppDir; private ServerSocket mRunLock = null; diff --git a/win_installer/create_installer.nsi b/win_installer/create_installer.nsi index 9e566514..b62ccb5e 100644 --- a/win_installer/create_installer.nsi +++ b/win_installer/create_installer.nsi @@ -13,7 +13,7 @@ ;Defines !define APPNAME "Kontalk Desktop Client" -!define VERSION "3.0.4" +!define VERSION "3.1" !define JARNAME "KontalkDesktopApp.jar" !define WEBSITE "kontalk.org" !define ICON "kontalk.ico"