diff options
author | steckbrief <steckbrief@chefmail.de> | 2018-05-05 20:28:04 +0200 |
---|---|---|
committer | steckbrief <steckbrief@chefmail.de> | 2018-05-05 20:28:04 +0200 |
commit | b0780224b5bdd68d74ef514e64e14ce9d37d7b90 (patch) | |
tree | 04311ea20090216b4897bcb507d79c7e02f76eae /src/main/java/de | |
parent | 853f50e43f68599774469dd12240f35800144991 (diff) |
introduces new message state model
Diffstat (limited to 'src/main/java/de')
57 files changed, 1872 insertions, 1144 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlMessageParser.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlMessageParser.java new file mode 100644 index 00000000..d77db0d4 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlMessageParser.java @@ -0,0 +1,37 @@ +package de.thedevstack.conversationsplus.crypto.axolotl; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + */ +public class AxolotlMessageParser { + public static Message parseAxolotlChat(Element axolotlMessage, Jid counterpart, Jid from, Conversation conversation) { + Jid origin; + if (conversation.getMode() == Conversation.MODE_MULTI) { + origin = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); + if (origin == null) { + Logging.d(Config.LOGTAG,"axolotl message in non anonymous conference received"); + return null; + } + } else { + origin = from; + } + + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid()); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL); + finishedMessage.setFingerprint(plaintextMessage.getFingerprint()); + Logging.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); + } + + return finishedMessage; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java index 9cf540a3..3569a7a5 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/axolotl/AxolotlServiceImpl.java @@ -39,6 +39,7 @@ import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.parser.IqParser; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.CryptoHelper; @@ -944,7 +945,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo public void run() { XmppAxolotlMessage axolotlMessage = encrypt(message); if (axolotlMessage == null) { - MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.FAILED); //mXmppConnectionService.updateConversationUi(); } else { Log.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrMessageParser.java b/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrMessageParser.java new file mode 100644 index 00000000..8791bff3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrMessageParser.java @@ -0,0 +1,67 @@ +package de.thedevstack.conversationsplus.crypto.otr; + +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; + +import de.thedevstack.conversationsplus.crypto.OtrService; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + */ +public class OtrMessageParser { + public static Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { + String presence; + if (from.isBareJid()) { + presence = ""; + } else { + presence = from.getResourcepart(); + } + if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + conversation.startOtrSession(presence,false); + } else { + String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + conversation.startOtrSession(presence, false); + } + } + try { + conversation.setLastReceivedOtrMessageId(id); + Session otrSession = conversation.getOtrSession(); + body = otrSession.transformReceiving(body); + SessionStatus status = otrSession.getSessionStatus(); + if (body == null && status == SessionStatus.ENCRYPTED) { + XmppConnectionServiceAccessor.xmppConnectionService.onOtrSessionEstablished(conversation); + return null; + } else if (body == null && status == SessionStatus.FINISHED) { + conversation.resetOtrSession(); + UiUpdateHelper.updateConversationUi(); + return null; + } else if (body == null || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + return null; + } + final OtrService otrService = conversation.getAccount().getOtrService(); + Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR); + finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey())); + conversation.setLastReceivedOtrMessageId(null); + + return finishedMessage; + } catch (Exception e) { + conversation.resetOtrSession(); + return null; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrUtil.java b/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrUtil.java new file mode 100644 index 00000000..97075455 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/otr/OtrUtil.java @@ -0,0 +1,39 @@ +package de.thedevstack.conversationsplus.crypto.otr; + +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.xmpp.Forwarded; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +/** + */ +public class OtrUtil { + public static boolean isOtrSessionActive(Conversation conversation) { + return null != conversation && null != conversation.getOtrSession(); + } + + public static boolean isCounterpartOfActiveOtrSession(Conversation conversation, Jid counterpart) { + SessionImpl otrSession = (null != conversation) ? conversation.getOtrSession() : null; + SessionID sessionID = (null != otrSession) ? otrSession.getSessionID() : null; + String userID = (null != sessionID) ? sessionID.getUserID() : null; + + return null != userID && null != counterpart && userID.equals(counterpart.getResourcepart()); + } + + public static boolean isValidOtrMessagePacket(MessagePacket messagePacket, Account account) { + Jid to = messagePacket.getTo(); + boolean isProperlyAddressed = (to != null) && (!to.isBareJid() || account.countPresences() <= 1); + boolean isNotTypeGroupChat = messagePacket.getType() != MessagePacket.TYPE_GROUPCHAT; + boolean isNotForwarded = !messagePacket.hasChildRecursive(Forwarded.FORWARDED); + + return isProperlyAddressed && isNotTypeGroupChat && isNotForwarded; + } + + public static boolean isOtrBody(String body) { + return body != null && body.startsWith("?OTR"); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java index 2edd1135..0cce63f4 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -16,12 +16,14 @@ import org.json.JSONObject; import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; +import java.util.NavigableSet; +import java.util.TreeSet; import de.thedevstack.conversationsplus.dto.LoadAvatarFor; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; @@ -66,7 +68,14 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata private Jid nextCounterpart; - protected final ArrayList<Message> messages = new ArrayList<>(); + protected final TreeSet<Message> messages = new TreeSet<>(new Comparator<Message>() { + @Override + public int compare(Message message, Message otherMessage) { + long thisTimeSent = message.getTimeSent(); + long otherTimeSent = otherMessage.getTimeSent(); + return (thisTimeSent < otherTimeSent ? -1 : (thisTimeSent == otherTimeSent ? 0 : 1)); + } + }); protected Account account = null; private transient SessionImpl otrSession; @@ -100,8 +109,8 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public Message findUnsentMessageWithUuid(String uuid) { synchronized(this.messages) { for (final Message message : this.messages) { - final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + if ((MessageUtil.isOutgoingMessage(message) + && (MessageStatus.WAITING == message.getMessageStatus() || MessageStatus.TRANSMITTING == message.getMessageStatus())) && message.getUuid().equals(uuid)) { return message; } } @@ -112,7 +121,7 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public void findWaitingMessages(OnMessageFound onMessageFound) { synchronized (this.messages) { for(Message message : this.messages) { - if (message.getStatus() == Message.STATUS_WAITING) { + if (MessageUtil.isOutgoingMessage(message) && MessageStatus.WAITING == message.getMessageStatus()) { onMessageFound.onMessageFound(message); } } @@ -193,7 +202,9 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata final int size = messages.size(); final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; if (size > maxsize) { - this.messages.subList(0, size - maxsize).clear(); + for (int i = 0; i < size - maxsize; i++) { + this.messages.pollFirst(); + } } } } @@ -201,7 +212,7 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { synchronized (this.messages) { for (Message message : this.messages) { - if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) + if ((MessageUtil.isOutgoingMessage(message) && (MessageStatus.WAITING == message.getMessageStatus() || MessageStatus.TRANSMITTING == message.getMessageStatus())) && (message.getEncryption() == encryptionType)) { onMessageFound.onMessageFound(message); } @@ -209,23 +220,25 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata } } - public void findUnsentTextMessages(OnMessageFound onMessageFound) { - synchronized (this.messages) { - for (Message message : this.messages) { - if (message.getType() != Message.TYPE_IMAGE - && message.getStatus() == Message.STATUS_UNSEND) { - onMessageFound.onMessageFound(message); - } - } - } - } + public void findOugoingTextMessagesInTransmission(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getType() != Message.TYPE_IMAGE // TODO: Check if we should resend everything + && MessageUtil.isOutgoingMessage(message) + && MessageStatus.TRANSMITTING == message.getMessageStatus()) { + onMessageFound.onMessageFound(message); + } + } + } + } public Message findSentMessageWithUuidOrRemoteId(String id) { synchronized (this.messages) { for (Message message : this.messages) { if (id.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { + || (MessageUtil.isOutgoingMessage(message) + && MessageUtil.isMessageTransmittedOrDisplayedOrReceived(message) + && id.equals(message.getRemoteMsgId()))) { return message; } } @@ -233,24 +246,6 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata return null; } - public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { - synchronized (this.messages) { - for(int i = this.messages.size() - 1; i >= 0; --i) { - Message message = messages.get(i); - if (counterpart.equals(message.getCounterpart()) - && ((message.getStatus() == Message.STATUS_RECEIVED) == received) - && (carbon == message.isCarbon() || received) ) { - if (id.equals(message.getRemoteMsgId())) { - return message; - } else { - return null; - } - } - } - } - return null; - } - public Message findSentMessageWithUuid(String id) { synchronized (this.messages) { for (Message message : this.messages) { @@ -350,33 +345,42 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata } public boolean isRead() { - return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + return (this.messages.size() == 0) || this.messages.last().isRead(); } + /** + * Marks messages as read, starting with the latest message. + * Stops iterating at the first messages that is already read + * @return List of the messages which are newly marked as read + */ public List<Message> markRead() { final List<Message> unread = new ArrayList<>(); synchronized (this.messages) { - for(Message message : this.messages) { + for (Message message : this.messages.descendingSet()) { if (!message.isRead()) { message.markRead(); unread.add(message); - } + } else { + break; + } } } return unread; } public Message getLatestMarkableMessage() { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED - && this.messages.get(i).markable) { - if (this.messages.get(i).isRead()) { + NavigableSet<Message> descendingSet = this.messages.descendingSet(); + for (Message message : descendingSet) { + if (MessageUtil.isIncomingMessage(message) + && message.markable) { + if (message.isRead()) { return null; } else { - return this.messages.get(i); + return message; } - } + } } + return null; } @@ -386,7 +390,7 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata message.setTime(getCreated()); return message; } else { - Message message = this.messages.get(this.messages.size() - 1); + Message message = this.messages.last(); message.setConversation(this); return message; } @@ -621,9 +625,8 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata private int getMostRecentlyUsedOutgoingEncryption() { synchronized (this.messages) { - for(int i = this.messages.size() -1; i >= 0; --i) { - final Message m = this.messages.get(i); - if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) { + for (Message m : this.messages.descendingSet()) { + if (!m.isCarbon() && MessageUtil.isOutgoingMessage(m)) { final int e = m.getEncryption(); if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { return Message.ENCRYPTION_PGP; @@ -638,10 +641,9 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata private int getMostRecentlyUsedIncomingEncryption() { synchronized (this.messages) { - for(int i = this.messages.size() -1; i >= 0; --i) { - final Message m = this.messages.get(i); - if (m.getStatus() == Message.STATUS_RECEIVED) { - final int e = m.getEncryption(); + for (Message message : this.messages.descendingSet()) { + if (MessageUtil.isIncomingMessage(message)) { + final int e = message.getEncryption(); if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { return Message.ENCRYPTION_PGP; } else { @@ -734,8 +736,8 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public boolean hasDuplicateMessage(Message message) { synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).equals(message)) { + for (Message m : this.messages.descendingSet()) { + if (m.equals(message)) { return true; } } @@ -745,9 +747,8 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public Message findSentMessageWithBody(String body) { synchronized (this.messages) { - for (int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { + for (Message message : this.messages.descendingSet()) { + if (MessageUtil.isMessageSent(message)) { String otherBody = message.getBody(); if (otherBody != null && otherBody.equals(body)) { return message; @@ -760,9 +761,8 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata public long getLastMessageTransmitted() { synchronized (this.messages) { - for(int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) { + for (Message message : this.messages.descendingSet()) { + if (MessageUtil.isIncomingMessage(message) || message.isCarbon()) { return message.getTimeSent(); } } @@ -880,48 +880,20 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata } } - public void prepend(Message message) { - message.setConversation(this); - synchronized (this.messages) { - this.messages.add(0,message); - } - } - public void addAll(int index, List<Message> messages) { synchronized (this.messages) { - this.messages.addAll(index, messages); + this.messages.addAll(messages); } account.getPgpDecryptionService().addAll(messages); } - public void sort() { - synchronized (this.messages) { - Collections.sort(this.messages, new Comparator<Message>() { - @Override - public int compare(Message left, Message right) { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - } - }); - for(Message message : this.messages) { - message.untie(); - } - } - } - public int unreadCount() { if (getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) { return 0; } synchronized (this.messages) { int count = 0; - for(int i = this.messages.size() - 1; i >= 0; --i) { - Message message = this.messages.get(i); + for (Message message : this.messages.descendingSet()) { if (message.isRead()) { return count; } @@ -933,6 +905,14 @@ public class Conversation extends AbstractEntity implements Blockable, LoadAvata } } + public Message getMessageBefore(Message message) { + return this.messages.lower(message); + } + + public Message getMessageAfter(Message message) { + return this.messages.higher(message); + } + public class Smp { public static final int STATUS_NONE = 0; public static final int STATUS_CONTACT_REQUESTED = 1; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java index 36ab036e..86adb4af 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -9,8 +9,11 @@ import java.net.URL; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; import de.thedevstack.conversationsplus.dto.LoadAvatarFor; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageConfirmation; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.utils.FileUtils; -import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.MessageParserUtil; import de.thedevstack.conversationsplus.utils.MimeUtils; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -19,16 +22,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { public static final String TABLENAME = "messages"; - public static final int STATUS_RECEIVED = 0; - public static final int STATUS_UNSEND = 1; - public static final int STATUS_SEND = 2; - public static final int STATUS_SEND_FAILED = 3; - public static final int STATUS_WAITING = 5; - public static final int STATUS_OFFERED = 6; - public static final int STATUS_SEND_RECEIVED = 7; - public static final int STATUS_SEND_DISPLAYED = 8; - public static final int STATUS_SEND_CANCELED = 9; // FIXME This bullshit is needed until status is handled more properly - public static final int ENCRYPTION_NONE = 0; public static final int ENCRYPTION_PGP = 1; public static final int ENCRYPTION_OTR = 2; @@ -69,11 +62,13 @@ public class Message extends AbstractEntity implements LoadAvatarFor { protected String encryptedBody; protected long timeSent; protected int encryption; - protected int status; protected int type; + /** + * Indicates that a message was received via + * Message Carbons (XEP-0280) + */ protected boolean carbon = false; protected boolean oob = false; - protected String edited = null; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -87,16 +82,29 @@ public class Message extends AbstractEntity implements LoadAvatarFor { private boolean httpUploaded; private FileParams fileParams; + /** + * The direction of the message, + * incoming is a msg received from a jid, + * outgoing is a msg sent to a jid + */ + private MessageDirection direction; + private MessageStatus messageStatus; + /** + * Indicates that a message was received via + * Message Archive Management (XEP-0313) + */ + private boolean mamReceived; - private Message() { + /** + * Whether Delivery Receipts (XEP-0184) and/ or Chat Markers (XEP-0333) + * need to be requested and processed (outgoing msgs) or sent (incoming msgs) + */ + private MessageConfirmation confirmation; + public Message() { } public Message(Conversation conversation, String body, int encryption) { - this(conversation, body, encryption, STATUS_UNSEND); - } - - public Message(Conversation conversation, String body, int encryption, int status) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().toBareJid(), @@ -104,7 +112,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { body, System.currentTimeMillis(), encryption, - status, TYPE_TEXT, false, null, @@ -112,17 +119,16 @@ public class Message extends AbstractEntity implements LoadAvatarFor { null, null, true, - null, false); this.conversation = conversation; } private Message(final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, + final int encryption, final int type, final boolean carbon, final String remoteMsgId, final String relativeFilePath, final String serverMsgId, final String fingerprint, final boolean read, - final String edited, final boolean oob) { + final boolean oob) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -130,7 +136,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.body = body; this.timeSent = timeSent; this.encryption = encryption; - this.status = status; this.type = type; this.carbon = carbon; this.remoteMsgId = remoteMsgId; @@ -138,7 +143,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.serverMsgId = serverMsgId; this.axolotlFingerprint = fingerprint; this.read = read; - this.edited = edited; this.oob = oob; } @@ -172,7 +176,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { cursor.getString(cursor.getColumnIndex(BODY)), cursor.getLong(cursor.getColumnIndex(TIME_SENT)), cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), - cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), @@ -180,18 +183,9 @@ public class Message extends AbstractEntity implements LoadAvatarFor { cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), cursor.getString(cursor.getColumnIndex(FINGERPRINT)), cursor.getInt(cursor.getColumnIndex(READ)) > 0, - cursor.getString(cursor.getColumnIndex(EDITED)), cursor.getInt(cursor.getColumnIndex(OOB)) > 0); } - public static Message createStatusMessage(Conversation conversation, String body) { - final Message message = new Message(); - message.setType(Message.TYPE_STATUS); - message.setConversation(conversation); - message.setBody(body); - return message; - } - @Override public ContentValues getContentValues() { ContentValues values = new ContentValues(); @@ -210,7 +204,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { values.put(BODY, body); values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); - values.put(STATUS, status); values.put(TYPE, type); values.put(CARBON, carbon ? 1 : 0); values.put(REMOTE_MSG_ID, remoteMsgId); @@ -218,7 +211,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { values.put(SERVER_MSG_ID, serverMsgId); values.put(FINGERPRINT, axolotlFingerprint); values.put(READ,read ? 1 : 0); - values.put(EDITED, edited); values.put(OOB, oob ? 1 : 0); return values; } @@ -276,14 +268,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.encryption = encryption; } - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - public String getRelativeFilePath() { return this.relativeFilePath; } @@ -351,10 +335,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.carbon = carbon; } - public boolean edited() { - return this.edited != null; - } - public void setTrueCounterpart(Jid trueCounterpart) { this.trueCounterpart = trueCounterpart; } @@ -367,6 +347,37 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.transferable = transferable; } + + public boolean isMamReceived() { + return mamReceived; + } + + public void setMamReceived(boolean mamReceived) { + this.mamReceived = mamReceived; + } + + public MessageConfirmation getConfirmation() { + return confirmation; + } + + public void setConfirmation(MessageConfirmation confirmation) { + this.confirmation = confirmation; + } + + @Override + public int hashCode() { + if (null != this.getServerMsgId()) { + return this.getServerMsgId().hashCode(); + } else if (null != this.getRemoteMsgId()) { + return this.getRemoteMsgId().hashCode(); + } else if (null != this.getUuid()) { + return this.getUuid().hashCode(); + } else if (null != this.getBody()){ + return this.getBody().hashCode(); + } + return super.hashCode(); + } + public boolean equals(Message message) { if (this.getServerMsgId() != null && message.getServerMsgId() != null) { return this.getServerMsgId().equals(message.getServerMsgId()); @@ -400,12 +411,7 @@ public class Message extends AbstractEntity implements LoadAvatarFor { public Message next() { synchronized (this.conversation.messages) { if (this.mNextMessage == null) { - int index = this.conversation.messages.indexOf(this); - if (index < 0 || index >= this.conversation.messages.size() - 1) { - this.mNextMessage = null; - } else { - this.mNextMessage = this.conversation.messages.get(index + 1); - } + this.mNextMessage = this.conversation.messages.higher(this); } return this.mNextMessage; } @@ -414,12 +420,7 @@ public class Message extends AbstractEntity implements LoadAvatarFor { public Message prev() { synchronized (this.conversation.messages) { if (this.mPreviousMessage == null) { - int index = this.conversation.messages.indexOf(this); - if (index <= 0 || index > this.conversation.messages.size()) { - this.mPreviousMessage = null; - } else { - this.mPreviousMessage = this.conversation.messages.get(index - 1); - } + this.mPreviousMessage = this.conversation.messages.lower(this); } return this.mPreviousMessage; } @@ -439,7 +440,7 @@ public class Message extends AbstractEntity implements LoadAvatarFor { public boolean trusted() { Contact contact = this.getContact(); - return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); + return contact != null && contact.trusted(); } public boolean fixCounterpart() { @@ -466,10 +467,6 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.uuid = uuid; } - public String getEditedId() { - return edited; - } - public void setOob(boolean isOob) { this.oob = isOob; } @@ -505,17 +502,19 @@ public class Message extends AbstractEntity implements LoadAvatarFor { mTreatAsDownloadAble = Decision.NEVER; } + @Deprecated public void setTreatAsDownloadable(Decision downloadable) { this.mTreatAsDownloadAble = downloadable; } + @Deprecated public Decision treatAsDownloadable() { // only test this ones, body will not change if (mTreatAsDownloadAble != Decision.NOT_DECIDED) { return mTreatAsDownloadAble; } - MessageUtil.extractFileParamsFromBody(this); + MessageParserUtil.extractFileParamsFromBody(this); return this.mTreatAsDownloadAble; } @@ -561,7 +560,7 @@ public class Message extends AbstractEntity implements LoadAvatarFor { private int getPreviousEncryption() { for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){ - if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + if( iterator.isCarbon() || MessageDirection.IN == iterator.getDirection()) { continue; } return iterator.getEncryption(); @@ -571,7 +570,7 @@ public class Message extends AbstractEntity implements LoadAvatarFor { private int getNextEncryption() { for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){ - if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + if( iterator.isCarbon() || MessageDirection.IN == iterator.getDirection()) { continue; } return iterator.getEncryption(); @@ -606,6 +605,22 @@ public class Message extends AbstractEntity implements LoadAvatarFor { this.fileParams = params; } + public MessageDirection getDirection() { + return direction; + } + + public void setDirection(MessageDirection direction) { + this.direction = direction; + } + + public MessageStatus getMessageStatus() { + return messageStatus; + } + + public void setMessageStatus(MessageStatus messageStatus) { + this.messageStatus = messageStatus; + } + private static int getCleanedEncryption(int encryption) { if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { return ENCRYPTION_PGP; diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/MessageConfirmation.java b/src/main/java/de/thedevstack/conversationsplus/enums/MessageConfirmation.java new file mode 100644 index 00000000..bd4a9ccb --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/enums/MessageConfirmation.java @@ -0,0 +1,23 @@ +package de.thedevstack.conversationsplus.enums; + +/** + * This enum represents the different combinations + * of confirmations which need to be requested, processed + * or sent. + */ +public enum MessageConfirmation { + NONE, + /** + * Message Delivery Receipts (XEP-0184) + */ + DELIVERY_RECEIPT, + /** + * Chat Markers (XEP-0333) + */ + CHAT_MARKERS, + /** + * Message Delivery Receipts (XEP-0184) and + * Chat Markers (XEP-0333) + */ + DELIVERY_RECEIPT_CHAT_MARKERS; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/MessageDirection.java b/src/main/java/de/thedevstack/conversationsplus/enums/MessageDirection.java new file mode 100644 index 00000000..59746194 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/enums/MessageDirection.java @@ -0,0 +1,8 @@ +package de.thedevstack.conversationsplus.enums; + +/** + */ +public enum MessageDirection { + OUT, + IN; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/MessageStatus.java b/src/main/java/de/thedevstack/conversationsplus/enums/MessageStatus.java new file mode 100644 index 00000000..8a31a325 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/enums/MessageStatus.java @@ -0,0 +1,54 @@ +package de.thedevstack.conversationsplus.enums; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +/** + */ +public enum MessageStatus { + WAITING, + TRANSMITTING, + TRANSMITTED, + FAILED, + CANCELED, + DISPLAYED, + RECEIVED; + + static { + /* + * Initialization of valid next states in this block, + * because the enum values need to be known at initialization. + * If trying to initialize in Constructor, then only forward transitions are possible., + * no way to handle jumping back. + */ + WAITING.initValidNextStates(TRANSMITTING); + TRANSMITTING.initValidNextStates(FAILED, TRANSMITTED, CANCELED); + CANCELED.initValidNextStates(WAITING); + FAILED.initValidNextStates(WAITING); + + // Restrict some transitions to be only valid for outgoing messages + TRANSMITTED.initValidNextStatesForDirection(MessageDirection.OUT, RECEIVED, DISPLAYED); + RECEIVED.initValidNextStatesForDirection(MessageDirection.OUT, DISPLAYED); + } + + private List<MessageStatus> validNextStates; + private HashMap<MessageDirection, List<MessageStatus>> directionRestricted = new HashMap<>(); + + private void initValidNextStates(MessageStatus... validNextStates) { + this.validNextStates = Arrays.asList(validNextStates); + } + + private void initValidNextStatesForDirection(MessageDirection direction, MessageStatus... newStatus) { + this.directionRestricted.put(direction, Arrays.asList(newStatus)); + } + + public boolean isTransitionToStateAllowed(MessageStatus newStatus, MessageDirection direction) { + boolean hasNoValidNextStates = this.validNextStates.isEmpty() && (!this.directionRestricted.containsKey(direction) || this.directionRestricted.get(direction).isEmpty()); + boolean isValidNextState = this.validNextStates.contains(newStatus); + boolean isValidNextStateForDirection = (this.directionRestricted.containsKey(direction) && this.directionRestricted.get(direction).contains(newStatus)); + return hasNoValidNextStates // no valid next states -> every new state is allowed + || isValidNextState // if newStatus is conditionless + || isValidNextStateForDirection; // if the newStatus is valid for the direction + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java index 2e580f09..13494baf 100644 --- a/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java +++ b/src/main/java/de/thedevstack/conversationsplus/generator/MessageGenerator.java @@ -4,7 +4,6 @@ import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -25,6 +24,10 @@ import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { + @Deprecated + /** + * Moved to messaging.MessageGenerator + */ private MessagePacket preparePacket(Message message) { Conversation conversation = message.getConversation(); Account account = conversation.getAccount(); @@ -48,9 +51,7 @@ public class MessageGenerator extends AbstractGenerator { } packet.setFrom(account.getJid()); packet.setId(message.getUuid()); - if (message.edited()) { - packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId()); - } + return packet; } @@ -112,7 +113,7 @@ public class MessageGenerator extends AbstractGenerator { FileParams fileParams = message.getFileParams(); content = message.getBody(); if (message.isHttpUploaded()) { - packet.addChild(new HttpUploadHint()); + packet.addChild(HttpUploadHint.HTTP_UPLOAD_HINT.getXmlElement()); } packet.addChild("x","jabber:x:oob").addChild("url").setContent(content); if (fileParams.getWidth() > 0 && fileParams.getHeight() > 0) { @@ -192,17 +193,6 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, MessagePacket originalMessage, ArrayList<String> namespaces, int type) { - MessagePacket receivedPacket = new MessagePacket(); - receivedPacket.setType(type); - receivedPacket.setTo(originalMessage.getFrom()); - receivedPacket.setFrom(account.getJid()); - for(String namespace : namespaces) { - receivedPacket.addChild("received", namespace).setAttribute("id", originalMessage.getId()); - } - return receivedPacket; - } - public MessagePacket generateOtrError(Jid to, String id, String errorText) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_ERROR); diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java index 889adbac..a849d26b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -3,35 +3,37 @@ package de.thedevstack.conversationsplus.parser; import android.util.Log; import android.util.Pair; -import de.thedevstack.conversationsplus.entities.FileParams; -import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlMessageParser; +import de.thedevstack.conversationsplus.crypto.otr.OtrMessageParser; +import de.thedevstack.conversationsplus.crypto.otr.OtrUtil; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.services.filetransfer.http.download.AutomaticFileDownload; import de.thedevstack.conversationsplus.services.filetransfer.http.download.HttpRetrieveHead; +import de.thedevstack.conversationsplus.utils.ConversationUtil; +import de.thedevstack.conversationsplus.utils.MessageParserUtil; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; -import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacket; import de.thedevstack.conversationsplus.xmpp.avatar.AvatarPacketParser; import de.thedevstack.conversationsplus.xmpp.carbons.Carbons; +import de.thedevstack.conversationsplus.xmpp.chatmarkers.ChatMarkers; import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint; import de.thedevstack.conversationsplus.xmpp.mam.Mam; +import de.thedevstack.conversationsplus.xmpp.muc.MucPacketParser; import de.thedevstack.conversationsplus.xmpp.openpgp.OpenPgpXep; +import de.thedevstack.conversationsplus.xmpp.receipts.MessageDeliveryReceipts; import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacketReceiver; -import de.tzur.conversations.Settings; -import net.java.otr4j.session.Session; -import net.java.otr4j.session.SessionStatus; - -import java.util.ArrayList; import java.util.Set; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.Config; -import de.thedevstack.conversationsplus.crypto.OtrService; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlMessage; @@ -44,7 +46,6 @@ import de.thedevstack.conversationsplus.entities.MucOptions; import de.thedevstack.conversationsplus.services.avatar.AvatarService; import de.thedevstack.conversationsplus.services.mam.MessageArchiveService; import de.thedevstack.conversationsplus.services.XmppConnectionService; -import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnMessagePacketReceived; import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; @@ -72,117 +73,13 @@ public class MessageParser extends AbstractParser implements } return false; } else { + updateLastseen(packet, account, true); // Todo: Should the timestamp be extracted here? return conversation.setIncomingChatState(state); } } return false; } - private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { - String presence; - if (from.isBareJid()) { - presence = ""; - } else { - presence = from.getResourcepart(); - } - if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { - conversation.endOtrIfNeeded(); - } - if (!conversation.hasValidOtrSession()) { - conversation.startOtrSession(presence,false); - } else { - String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); - if (!foreignPresence.equals(presence)) { - conversation.endOtrIfNeeded(); - conversation.startOtrSession(presence, false); - } - } - try { - conversation.setLastReceivedOtrMessageId(id); - Session otrSession = conversation.getOtrSession(); - body = otrSession.transformReceiving(body); - SessionStatus status = otrSession.getSessionStatus(); - if (body == null && status == SessionStatus.ENCRYPTED) { - mXmppConnectionService.onOtrSessionEstablished(conversation); - return null; - } else if (body == null && status == SessionStatus.FINISHED) { - conversation.resetOtrSession(); - UiUpdateHelper.updateConversationUi(); - return null; - } else if (body == null || (body.isEmpty())) { - return null; - } - if (body.startsWith(CryptoHelper.FILETRANSFER)) { - String key = body.substring(CryptoHelper.FILETRANSFER.length()); - conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); - return null; - } - final OtrService otrService = conversation.getAccount().getOtrService(); - Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); - finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey())); - conversation.setLastReceivedOtrMessageId(null); - - return finishedMessage; - } catch (Exception e) { - conversation.resetOtrSession(); - return null; - } - } - - private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status) { - Message finishedMessage = null; - AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid()); - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage); - if(plaintextMessage != null) { - finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); - finishedMessage.setFingerprint(plaintextMessage.getFingerprint()); - Logging.d(Config.LOGTAG, AxolotlServiceImpl.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); - } - - return finishedMessage; - } - - private class Invite { - Jid jid; - String password; - Invite(Jid jid, String password) { - this.jid = jid; - this.password = password; - } - - public boolean execute(Account account) { - if (jid != null) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true); - if (!conversation.getMucOptions().online()) { - conversation.getMucOptions().setPassword(password); - DatabaseBackend.getInstance().updateConversation(conversation); - mXmppConnectionService.joinMuc(conversation); - UiUpdateHelper.updateConversationUi(); - } - return true; - } - return false; - } - } - - private Invite extractInvite(Element message) { - Element x = message.findChild("x", "http://jabber.org/protocol/muc#user"); - if (x != null) { - Element invite = x.findChild("invite"); - if (invite != null) { - Element pw = x.findChild("password"); - return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null); - } - } else { - x = message.findChild("x","jabber:x:conference"); - if (x != null) { - return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password")); - } - } - return null; - } - private static String extractStanzaId(Element packet, Jid by) { for(Element child : packet.getChildren()) { if (child.getName().equals("stanza-id") @@ -251,13 +148,15 @@ public class MessageParser extends AbstractParser implements } else if (error != null) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error); } - Message message = mXmppConnectionService.markMessage(account, + Message message = XmppConnectionServiceAccessor.xmppConnectionService.getMessage(account, from.toBareJid(), - packet.getId(), - Message.STATUS_SEND_FAILED); - if (message != null && message.getEncryption() == Message.ENCRYPTION_OTR) { - message.getConversation().endOtrIfNeeded(); - } + packet.getId()); + if (null != message) { + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.FAILED); + if (Message.ENCRYPTION_OTR == message.getEncryption()) { + message.getConversation().endOtrIfNeeded(); + } + } } return true; } @@ -269,326 +168,338 @@ public class MessageParser extends AbstractParser implements if (handleErrorMessage(account, original)) { return; } - final MessagePacket packet; + MessagePacket packet = null; Long timestamp = null; - final boolean isForwarded; boolean isCarbon = false; + boolean isMAMCatchup = false; String serverMsgId = null; + MessageDirection direction = null; + final Element fin = original.findChild("fin", Mam.NAMESPACE); + MessageArchiveService messageArchiveService = XmppConnectionServiceAccessor.xmppConnectionService.getMessageArchiveService(); if (fin != null) { - mXmppConnectionService.getMessageArchiveService().processFin(fin,original.getFrom()); + // Todo: Check for query.getWith() -> MAM Catchup -> Handle?? + messageArchiveService.processFin(fin,original.getFrom()); return; } final Element result = original.findChild("result", Mam.NAMESPACE); - final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid")); - if (query != null && query.validFrom(original.getFrom())) { - Pair<MessagePacket, Long> f = original.getForwardedMessagePacket("result", Mam.NAMESPACE); - if (f == null) { - return; - } - timestamp = f.second; - packet = f.first; - isForwarded = true; - serverMsgId = result.getAttribute("id"); - query.incrementMessageCount(); - } else if (query != null) { - Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); - return; - } else if (original.fromServer(account)) { - Pair<MessagePacket, Long> f; - f = original.getForwardedMessagePacket("received", Carbons.NAMESPACE); - f = f == null ? original.getForwardedMessagePacket("sent", Carbons.NAMESPACE) : f; - packet = f != null ? f.first : original; - if (handleErrorMessage(account, packet)) { - return; - } - timestamp = f != null ? f.second : null; - isCarbon = f != null; - isForwarded = isCarbon; - } else { - packet = original; - isForwarded = false; - } + boolean isMAM = result != null; + + if (isMAM) { + final MessageArchiveService.Query query = messageArchiveService.findQuery(result.getAttribute("queryid")); + if (null != query && query.validFrom(original.getFrom())) { + Pair<MessagePacket, Long> forwardedMessagePacket = original.getForwardedMessagePacket("result", Mam.NAMESPACE); + if (forwardedMessagePacket == null) { + return; + } + isMAMCatchup = null == query.getWith(); + timestamp = forwardedMessagePacket.second; + packet = forwardedMessagePacket.first; + serverMsgId = result.getAttribute("id"); + query.incrementMessageCount(); + } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); + return; + } + } else if (original.fromServer(account)) { // Todo: Check if carbons are not always from the own bare JID + Pair<MessagePacket, Long> forwardedMessagePacket = original.getForwardedMessagePacket("received", Carbons.NAMESPACE); + if (null != forwardedMessagePacket) { + direction = MessageDirection.IN; + } else { + forwardedMessagePacket = original.getForwardedMessagePacket("sent", Carbons.NAMESPACE); + if (null != forwardedMessagePacket) { + direction = MessageDirection.OUT; + } + } + + isCarbon = forwardedMessagePacket != null; + if (isCarbon) { + packet = forwardedMessagePacket.first; + timestamp = forwardedMessagePacket.second; + if (handleErrorMessage(account, packet)) { + return; + } + } + } + + if (null == packet) { + packet = original; + } + + if (null == timestamp) { + timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); + } + + final Jid from = packet.getFrom(); + + if (from == null) { + Log.d(Config.LOGTAG,"no from in: "+packet.toString()); + return; + } - if (timestamp == null) { - timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); - } final String body = packet.getBody(); - final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); - final String pgpEncrypted = packet.findChildContent(OpenPgpXep.ENCRYPTED_ELEMENT, OpenPgpXep.ENCRYPTED_NAMESPACE); - final Element oob = packet.findChild("x", "jabber:x:oob"); - final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url")); - final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - int status; + final String pgpEncrypted = (Config.supportOpenPgp()) ? packet.findChildContent(OpenPgpXep.ENCRYPTED_ELEMENT, OpenPgpXep.ENCRYPTED_NAMESPACE) : null; + final Element axolotlEncrypted = (Config.supportOmemo()) ? packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX) : null; final Jid counterpart; final Jid to = packet.getTo(); - final Jid from = packet.getFrom(); - final String remoteMsgId = packet.getId(); - if (from == null) { - Log.d(Config.LOGTAG,"no from in: "+packet.toString()); - return; - } - - boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; - boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() <= 1); - boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); if (packet.fromAccount(account)) { - status = Message.STATUS_SEND; + direction = (null == direction) ? MessageDirection.OUT : direction; counterpart = to != null ? to : account.getJid(); } else { - status = Message.STATUS_RECEIVED; + direction = (null == direction) ? MessageDirection.IN : direction; counterpart = from; } - Invite invite = extractInvite(packet); - if (invite != null && invite.execute(account)) { - return; - } - - if (extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) { - UiUpdateHelper.updateConversationUi(); - } - - if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat, query); - if (isTypeGroupChat) { - if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { - status = Message.STATUS_SEND_RECEIVED; - isCarbon = true; //not really carbon but received from another resource - if (MessageUtil.markMessage(conversation, remoteMsgId, status)) { - return; - } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { - Message message = conversation.findSentMessageWithBody(packet.getBody()); - if (message != null) { - MessageUtil.markMessage(message, status); - return; - } - } - } else { - status = Message.STATUS_RECEIVED; - } - } - Message message; - if (body != null && body.startsWith("?OTR") && Config.supportOtr()) { - if (!isForwarded && !isTypeGroupChat && isProperlyAddressed) { - message = parseOtrChat(body, from, remoteMsgId, conversation); - if (message == null) { - return; - } - } else { - Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed)); - message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); - } - } else if (pgpEncrypted != null && Config.supportOpenPgp()) { - message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - } else if (axolotlEncrypted != null && Config.supportOmemo()) { - Jid origin; - if (conversation.getMode() == Conversation.MODE_MULTI) { - origin = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); - if (origin == null) { - Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received"); - return; - } - } else { - origin = from; - } - message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status); - if (message == null) { - return; - } - } else { - message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); - } - - if (serverMsgId == null) { - serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer()); - } - message.setHttpUploaded(packet.hasChild(HttpUploadHint.ELEMENT_NAME, HttpUploadHint.NAMESPACE)); - if (message.isHttpUploaded()) { - message.setTreatAsDownloadable(Message.Decision.MUST); - } - - message.setCounterpart(counterpart); - message.setRemoteMsgId(remoteMsgId); - message.setServerMsgId(serverMsgId); - message.setCarbon(isCarbon); - message.setTime(timestamp); - message.setOob(isOob); - message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); - if (conversation.getMode() == Conversation.MODE_MULTI) { - Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); - message.setTrueCounterpart(trueCounterpart); - if (trueCounterpart != null) { - updateLastseen(timestamp, account, trueCounterpart, false); - } - if (!isTypeGroupChat) { - message.setType(Message.TYPE_PRIVATE); - } - } else { - updateLastseen(timestamp, account, packet.getFrom(), true); - } + if (MucPacketParser.extractAndExecuteInvite(account, packet)) { + return; + } - boolean checkForDuplicates = query != null - || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) - || message.getType() == Message.TYPE_PRIVATE; - if (checkForDuplicates && conversation.hasDuplicateMessage(message)) { - Logging.d(Config.LOGTAG, "skipping duplicate message from '" + message.getCounterpart().toString() + "' with remote id " + message.getRemoteMsgId()); - return; - } - - if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { - conversation.prepend(message); - } else { - conversation.add(message); - } - - if (message.getEncryption() == Message.ENCRYPTION_PGP) { - conversation.getAccount().getPgpDecryptionService().add(message); - } - - if (query == null || query.getWith() == null) { //either no mam or catchup - if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { - Logging.d("markRead", "MessageParser.onMessagePacketReceived1 (" + conversation.getName() + ")"); - mXmppConnectionService.markRead(conversation); - if (query == null) { - account.activateGracePeriod(); - } - } else { - // only not mam messages should be marked as unread - if (query == null) { - message.markUnread(); - } - } - } + Conversation conversation = null; - if (query == null) { - UiUpdateHelper.updateConversationUi(); - } + if ((body != null || pgpEncrypted != null || axolotlEncrypted != null)) { + boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; + conversation = XmppConnectionServiceAccessor.xmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); - if (Settings.CONFIRM_MESSAGE_READ && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { - sendMessageReceipts(account, packet); - } + final String remoteMsgId = packet.getId(); - if (message.getStatus() == Message.STATUS_RECEIVED - && conversation.getOtrSession() != null - && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getCounterpart().getResourcepart())) { - conversation.endOtrIfNeeded(); - } + Message message = null; + if (Config.supportOtr() && OtrUtil.isOtrBody(body)) { + if (OtrUtil.isValidOtrMessagePacket(packet, account)) { + message = OtrMessageParser.parseOtrChat(body, counterpart, remoteMsgId, conversation); + } else { + Logging.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": ignoring OTR message from "+counterpart+", addressed to: " + to); + } + } else { + message = this.createMessage(conversation, counterpart, body, pgpEncrypted, axolotlEncrypted); + } - MessageUtil.extractFileParamsFromBody(message); - FileParams fileParams = message.getFileParams(); - if (message.treatAsDownloadable() != Message.Decision.NEVER && message.treatAsDownloadable() != Message.Decision.NOT_DECIDED) { - if (null != fileParams) { - fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); - } - } - if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { - DatabaseBackend.getInstance().createMessage(message); - } - if (message.trusted() - && message.treatAsDownloadable() != Message.Decision.NEVER - && ConversationsPlusPreferences.autoAcceptFileSize() > 0 - && (message.isHttpUploaded() || MessageUtil.hasDownloadableLink(message))) { // Can this be checked by MessageUtil.needsDownload(message) ?? - HttpRetrieveHead hrh = new HttpRetrieveHead(message); - hrh.setListener(new AutomaticFileDownload(true)); - hrh.retrieveAndSetContentTypeAndLength(); - } else { - if (query == null) { - mXmppConnectionService.getNotificationService().push(message); - } else if (query.getWith() == null) { // mam catchup - /* - Like suggested in https://bugs.thedevstack.de/task/156 user should be notified - in some other way of loaded messages. - */ - // mXmppConnectionService.getNotificationService().pushFromBacklog(message); - } - } + if (null != message) { + message.setDirection(direction); + message.setMessageStatus(MessageStatus.TRANSMITTING); + message.setCarbon(isCarbon); + message.setMamReceived(isMAM); + message.setConfirmation(MessageParserUtil.extractMessageConfirmation(packet)); + + if (isTypeGroupChat + && this.fixMessageDirectionForGroupChatIfNecessary(message, counterpart)) { + return; + } + + this.handleMessagePacketWithBodyOrEncryptedContent(message, packet, account, conversation, counterpart, isTypeGroupChat, remoteMsgId, serverMsgId, timestamp); + + this.markMessagesUnreadIfNecessary(message, isMAM, isMAMCatchup); + + if (!isMAM) { + UiUpdateHelper.updateConversationUi(); + } + + if (message.trusted() + && MessageStatus.TRANSMITTING == message.getMessageStatus() // Todo: Every message should be in transmitting state here + && ConversationsPlusPreferences.autoAcceptFileSize() > 0 + && (message.isHttpUploaded() || MessageUtil.hasDownloadableLink(message))) { // Can this be checked by MessageUtil.needsDownload(message) ?? + HttpRetrieveHead hrh = new HttpRetrieveHead(message); + hrh.setListener(new AutomaticFileDownload()); + hrh.retrieveAndSetContentTypeAndLength(); + } else { + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTED); + } + } } else if (!packet.hasChild("body")){ //no body - if (isTypeGroupChat) { - Conversation conversation = mXmppConnectionService.find(account, from.toBareJid()); - if (packet.hasChild("subject")) { - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); - String subject = packet.findChildContent("subject"); - conversation.getMucOptions().setSubject(subject); - final Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null && bookmark.getBookmarkName() == null) { - if (bookmark.setBookmarkName(subject)) { - mXmppConnectionService.pushBookmarks(account); - } - } - UiUpdateHelper.updateConversationUi(); - return; - } - } - - if (conversation != null && isMucStatusMessage) { - for (Element child : mucUserElement.getChildren()) { - if (child.getName().equals("status") - && MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) { - mXmppConnectionService.fetchConferenceConfiguration(conversation); - } - } - } - } + this.handleMessagePacketWithoutBody(packet, account); } - Element received = packet.findChild("received", "urn:xmpp:chat-markers:0"); - if (received == null) { - received = packet.findChild("received", "urn:xmpp:receipts"); - } - if (received != null && !packet.fromAccount(account)) { - mXmppConnectionService.markMessage(account, from.toBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED); - } - Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); - if (displayed != null) { - if (packet.fromAccount(account)) { - Conversation conversation = mXmppConnectionService.find(account,counterpart.toBareJid()); - if (conversation != null) { - Logging.d("markRead", "MessageParser.onMessagePacketReceived2 (" + conversation.getName() + ")"); - mXmppConnectionService.markRead(conversation); - } - } else { - updateLastseen(timestamp, account, packet.getFrom(), true); - final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED); - Message message = displayedMessage == null ? null : displayedMessage.prev(); - while (message != null - && message.getStatus() == Message.STATUS_SEND_RECEIVED - && message.getTimeSent() < displayedMessage.getTimeSent()) { - MessageUtil.markMessage(message, Message.STATUS_SEND_DISPLAYED); - message = message.prev(); - } - } - } + this.handleChatState(conversation, account, packet, counterpart); - Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event"); - if (event != null) { - parseEvent(event, from, account); - } + this.handleMessageReceipts(packet, account, from, counterpart, timestamp); - String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick"); - if (nick != null) { - Contact contact = account.getRoster().getContact(from); - contact.setPresenceName(nick); - } - } + this.handleEvent(packet, account, from); - private void sendMessageReceipts(Account account, MessagePacket packet) { - ArrayList<String> receiptsNamespaces = new ArrayList<>(); - if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { - receiptsNamespaces.add("urn:xmpp:chat-markers:0"); - } - if (packet.hasChild("request", "urn:xmpp:receipts")) { - receiptsNamespaces.add("urn:xmpp:receipts"); - } - if (receiptsNamespaces.size() > 0) { - MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, - packet, - receiptsNamespaces, - packet.getType()); - XmppSendUtil.sendMessagePacket(account, receipt); - } + this.handleNick(packet, account, from); } + + private void markMessagesUnreadIfNecessary(Message message, boolean isMAM, boolean isCatchup) { + Conversation conversation = message.getConversation(); + Account account = conversation.getAccount(); + if (!isMAM || isCatchup) { //either no mam or catchup + if (MessageUtil.isOutgoingMessage(message)) { + Logging.d("markRead", "MessageParser.onMessagePacketReceived1 (" + conversation.getName() + ")"); + XmppConnectionServiceAccessor.xmppConnectionService.markRead(conversation); + if (!isMAM) { + account.activateGracePeriod(); + } + } else { + // only not mam messages should be marked as unread + if (!isMAM) { + message.markUnread(); + } + } + } + } + + private void handleChatState(Conversation conversation, Account account, MessagePacket packet, Jid counterpart) { + if (null == conversation) { + conversation = XmppConnectionServiceAccessor.xmppConnectionService.find(account, counterpart.toBareJid()); + } + if (extractChatState(conversation, packet)) { + UiUpdateHelper.updateConversationUi(); + } + } + + private boolean fixMessageDirectionForGroupChatIfNecessary(Message message, Jid counterpart) { + Conversation conversation = message.getConversation(); + if (MessageUtil.isIncomingMessage(message) + && counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { // Message is sent from me + message.setDirection(MessageDirection.OUT); // Not sure, but assuming + String remoteMsgId = message.getRemoteMsgId(); + if (MessageUtil.setAndSaveMessageStatus(conversation, remoteMsgId, MessageStatus.RECEIVED)) { // TODO: Check if this status is correct + return true; + } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { + Message existingMessage = conversation.findSentMessageWithBody(message.getBody()); + if (existingMessage != null) { + MessageUtil.setAndSaveMessageStatus(existingMessage, MessageStatus.RECEIVED); + return true; + } + } + } + return false; + } + + private Message createMessage(Conversation conversation, Jid counterpart, String body, + String pgpEncrypted, Element axolotlEncrypted) { + Message message; + + if (pgpEncrypted != null) { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP); + } else if (axolotlEncrypted != null) { + message = AxolotlMessageParser.parseAxolotlChat(axolotlEncrypted, counterpart, counterpart, conversation); + if (message == null) { + return null; + } + } else { + message = new Message(conversation, body, Message.ENCRYPTION_NONE); + } + + return message; + } + + private Message handleMessagePacketWithBodyOrEncryptedContent(Message message, MessagePacket packet, Account account, Conversation conversation, Jid counterpart, + boolean isTypeGroupChat, String remoteMsgId, String serverMsgId, long timestamp) { + if (serverMsgId == null) { + serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer()); + } + message.setHttpUploaded(packet.hasChild(HttpUploadHint.HTTP_UPLOAD_HINT)); + + message.setCounterpart(counterpart); + message.setRemoteMsgId(remoteMsgId); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + + if (conversation.getMode() == Conversation.MODE_MULTI) { + Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart()); + message.setTrueCounterpart(trueCounterpart); + if (trueCounterpart != null) { + updateLastseen(timestamp, account, trueCounterpart, false); + } + if (!isTypeGroupChat) { + message.setType(Message.TYPE_PRIVATE); + } + } else { + updateLastseen(timestamp, account, packet.getFrom(), true); + } + + conversation.add(message); + + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + conversation.getAccount().getPgpDecryptionService().add(message); + } + + if (MessageUtil.isIncomingMessage(message) + && OtrUtil.isOtrSessionActive(conversation) + && !OtrUtil.isCounterpartOfActiveOtrSession(conversation, message.getCounterpart())) { + conversation.endOtrIfNeeded(); + } + + MessageParserUtil.extractFileParamsFromBody(message); + + if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { + DatabaseBackend.getInstance().createMessage(message); + } + + return message; + } + + private void handleNick(MessagePacket packet, Account account, Jid from) { // TODO: Is this a nick change? + String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick"); + if (nick != null) { + Contact contact = account.getRoster().getContact(from); + contact.setPresenceName(nick); + } + } + + private void handleEvent(MessagePacket packet, Account account, Jid from) { + Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event"); + if (event != null) { + parseEvent(event, from, account); + } + } + + private void handleMessageReceipts(MessagePacket packet, Account account, Jid from, Jid counterpart, long timestamp) { + Element received = packet.findChild(ChatMarkers.RECEIVED); + if (received == null) { + received = packet.findChild(MessageDeliveryReceipts.RECEIVED); + } + if (received != null && !packet.fromAccount(account)) { + MessageUtil.markMessageAsReceived(account, from.toBareJid(), received.getAttribute("id")); + } + Element displayed = packet.findChild(ChatMarkers.DISPLAYED); + if (displayed != null) { + if (packet.fromAccount(account)) { + Conversation conversation = mXmppConnectionService.find(account, counterpart.toBareJid()); + if (conversation != null) { + Logging.d("markRead", "MessageParser.onMessagePacketReceived2 (" + conversation.getName() + ")"); + mXmppConnectionService.markRead(conversation); + } + } else { + updateLastseen(timestamp, account, packet.getFrom(), true); + Message message = XmppConnectionServiceAccessor.xmppConnectionService.getMessage(account, from.toBareJid(), displayed.getAttribute("id")); + if (null != message) { + ConversationUtil.markMessagesAsDisplayedUpToMessage(message); + } + } + } + } + + private void handleMessagePacketWithoutBody(MessagePacket packet, Account account) { + if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { + Jid from = packet.getFrom(); + final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); + Conversation conversation = mXmppConnectionService.find(account, from.toBareJid()); + if (packet.hasChild("subject")) { + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); + String subject = packet.findChildContent("subject"); + conversation.getMucOptions().setSubject(subject); + final Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null && bookmark.getBookmarkName() == null) { + if (bookmark.setBookmarkName(subject)) { + mXmppConnectionService.pushBookmarks(account); + } + } + UiUpdateHelper.updateConversationUi(); + return; + } + } + + if (conversation != null && isMucStatusMessage) { + for (Element child : mucUserElement.getChildren()) { + if (child.getName().equals("status") + && MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) { + mXmppConnectionService.fetchConferenceConfiguration(conversation); + } + } + } + } + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java index ed366a2c..a3caf503 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/PresenceParser.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import java.util.List; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.utils.AvatarUtil; @@ -241,12 +243,14 @@ public class PresenceParser extends AbstractParser implements if (statusMessage != null && !statusMessage.isEmpty() && conversation.countMessages() == 0) { - conversation.add(new Message( + Message message = new Message( conversation, statusMessage, - Message.ENCRYPTION_NONE, - Message.STATUS_RECEIVED - )); + Message.ENCRYPTION_NONE + ); + message.setDirection(MessageDirection.IN); + message.setMessageStatus(MessageStatus.TRANSMITTED); + conversation.add(message); } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java index e721afbb..fd227e5a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/DatabaseBackend.java @@ -58,7 +58,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; private static final int DATABASE_VERSION = 25; private static final int C_TO_CPLUS_VERSION_OFFSET = 1000; - private static final int CPLUS_DATABASE_VERSION = 3; + private static final int CPLUS_DATABASE_VERSION = 5; private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100; private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER); @@ -217,7 +217,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); } - if (oldVersion < 3 && newVersion >= 3) { + if ((oldVersion < 3 && newVersion >= 3) + || (oldVersion <= 5 && newVersion >= 4)) { MessageDatabaseAccess.upgrade(db, oldVersion, newVersion); } } @@ -719,25 +720,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public Pair<Long, String> getLastMessageReceived(Account account) { - Cursor cursor = null; - try { - SQLiteDatabase db = this.getReadableDatabase(); - String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; - String[] args = {account.getUuid()}; - cursor = db.rawQuery(sql, args); - if (cursor.getCount() == 0) { - return null; - } else { - cursor.moveToFirst(); - return new Pair<>(cursor.getLong(0), cursor.getString(1)); - } - } catch (Exception e) { - return null; - } finally { - if (cursor != null) { - cursor.close(); - } - } + return MessageDatabaseAccess.getLastMessageReceived(this.getReadableDatabase(), account.getUuid()); } private Cursor getCursorForSession(Account account, AxolotlAddress contact) { diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java index 43ae6ebb..cfef7a53 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java @@ -3,18 +3,25 @@ package de.thedevstack.conversationsplus.persistance.db.access; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.util.Pair; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.persistance.db.migrations.FileParamsBodyToDatabaseFieldsMigration; +import de.thedevstack.conversationsplus.persistance.db.migrations.MessageStatusMigration; import de.thedevstack.conversationsplus.utils.CryptoHelper; /** * */ public class MessageDatabaseAccess extends AbstractDatabaseAccess { + + private static final String TABLE_NAME_MESSAGES = "messages"; // since cplus db version 1 public static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters"; private static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload"; @@ -32,6 +39,9 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { private static final String COLUMN_NAME_MSG_PARAMS_FILE_IV = "file_iv"; private static final String COLUMN_NAME_MSG_PARAMS_FILE_KEY = "file_key"; private static final String COLUMN_NAME_MSG_PARAMS_ORIGINAL_FILE_NAME = "original_file_name"; + // since cplus db version 4 + private static final String COLUMN_NAME_MSG_STATUS = "status"; + private static final String COLUMN_NAME_MSG_DIRECTION = "direction"; private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V1 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " @@ -39,7 +49,24 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; - private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V3 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH + " INTEGER DEFAULT 0, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS + " TEXT DEFAULT 'UNDEFINED', " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_NAME + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_ORIGINAL_FILE_NAME + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_IV + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_KEY + " TEXT DEFAULT NULL, " + + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; + + private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V4 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " (" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, " + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, " + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', " @@ -54,6 +81,8 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL + " TEXT DEFAULT NULL, " + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_IV + " TEXT DEFAULT NULL, " + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_KEY + " TEXT DEFAULT NULL, " + + MessageDatabaseAccess.COLUMN_NAME_MSG_STATUS + " TEXT DEFAULT 'WAITING', " + + MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION + " TEXT DEFAULT 'OUT', " + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)"; public static void updateMessageParameters(SQLiteDatabase db, Message message, String uuid) { @@ -66,6 +95,8 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid()); additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0); additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION, message.getDirection().name()); + additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_STATUS, message.getMessageStatus().name()); if (null != message.getFileParams()) { FileParams fileParams = message.getFileParams(); additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE, fileParams.getMimeType()); @@ -104,6 +135,28 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { message.setTreatAsDownloadable(treatAsDownloadable); + String msgStatus = CursorHelper.getString(cursor, COLUMN_NAME_MSG_STATUS); + MessageStatus messageStatus = MessageStatus.WAITING; + try { + messageStatus = MessageStatus.valueOf(msgStatus); + } catch (IllegalArgumentException e) { + // Should only happen if the database is corrupted, but to be on the save side catch it here + Logging.e("db.msg", "Unknown MessageStatus found: '" + msgStatus + "'"); + } + + message.setMessageStatus(messageStatus); + + String msgDirection = CursorHelper.getString(cursor, COLUMN_NAME_MSG_DIRECTION); + MessageDirection messageDirection = MessageDirection.OUT; + try { + messageDirection = MessageDirection.valueOf(msgDirection); + } catch (IllegalArgumentException e) { + // Should only happen if the database is corrupted, but to be on the save side catch it here + Logging.e("db.msg", "Unknown MessageDirection found: '" + msgDirection + "'"); + } + + message.setDirection(messageDirection); + String status = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_STATUS); FileStatus fileStatus = FileStatus.UNDEFINED; if (null != status && !status.isEmpty()) { @@ -209,10 +262,96 @@ public class MessageDatabaseAccess extends AbstractDatabaseAccess { } cursor.close(); } + if (oldVersion < 4 && newVersion >= 4) { + Logging.d("db.upgrade.cplus", "Upgrade " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + ": new fields for status and direction"); + String[] columnDefinitions = { + MessageDatabaseAccess.COLUMN_NAME_MSG_STATUS + " TEXT DEFAULT 'WAITING'", + MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION + " TEXT DEFAULT 'OUT'" + }; + + addNewColumns(db, MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, columnDefinitions); + + Logging.d("db.upgrade.cplus", "Migrate integer status from " + Message.TABLENAME + "." + Message.STATUS + " to dedicated status and direction field in " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS); + Cursor cursor = db.rawQuery("SELECT " + Message.UUID + ", " + Message.STATUS + " FROM " + Message.TABLENAME, null); + while (cursor.moveToNext()) { + String uuid = CursorHelper.getString(cursor, Message.UUID); + int status = CursorHelper.getInt(cursor, Message.STATUS); + + MessageDirection direction = MessageStatusMigration.getMessageDirection(status); + MessageStatus newStatus = MessageStatusMigration.getMessageStatus(status); + + ContentValues parameterValues = new ContentValues(); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_STATUS, newStatus.name()); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION, direction.name()); + + db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, parameterValues, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", new String[] {uuid}); + } + cursor.close(); + } + if (oldVersion < 5 && newVersion >= 5) { // ONLY NEEDED FOR DEV + Logging.d("db.upgrade.cplus", "Fixing hick up in first try"); + String alterStatement = "ALTER TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " RENAME TO old_msg_params"; + db.execSQL(alterStatement); + + db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V4); + + db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " SELECT message_uuid, httpupload, treatasdownloadable_decision," + + "file_type, file_size, file_height, file_width, file_status, file_name, original_file_name, file_path, file_url, file_iv, file_key, status, 'IN' FROM old_msg_params"); + + db.execSQL("DROP TABLE old_msg_params"); + + Logging.d("db.upgrade.cplus", "Migrate integer status from " + Message.TABLENAME + "." + Message.STATUS + " to dedicated status and direction field in " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS); + Cursor cursor = db.rawQuery("SELECT " + Message.UUID + ", " + Message.STATUS + " FROM " + Message.TABLENAME, null); + while (cursor.moveToNext()) { + String uuid = CursorHelper.getString(cursor, Message.UUID); + int status = CursorHelper.getInt(cursor, Message.STATUS); + + MessageDirection direction = MessageStatusMigration.getMessageDirection(status); + MessageStatus newStatus = MessageStatusMigration.getMessageStatus(status); + + ContentValues parameterValues = new ContentValues(); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_STATUS, newStatus.name()); + parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION, direction.name()); + + db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, parameterValues, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", new String[] {uuid}); + } + cursor.close(); + } + if (oldVersion < 6 && newVersion >= 6) { + + } } public static void create(SQLiteDatabase db) { // Create Conversations+ related tables - db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE); + db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V4); + } + + public static Pair<Long, String> getLastMessageReceived(SQLiteDatabase db, String accountUuid) { + Cursor cursor = null; + try { + String sql = "select messages.timeSent,messages.serverMsgId " + + "from accounts " + + "join conversations on accounts.uuid=conversations.accountUuid " + + "join messages on conversations.uuid=messages.conversationUuid " + + "join " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " on messages.uuid=" + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "." + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " " + + "where accounts.uuid=? " + + "and (" + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "." + MessageDatabaseAccess.COLUMN_NAME_MSG_DIRECTION + "='IN'" + + " or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; + String[] args = {accountUuid}; + cursor = db.rawQuery(sql, args); + if (cursor.getCount() == 0) { + return null; + } else { + cursor.moveToFirst(); + return new Pair<>(cursor.getLong(0), cursor.getString(1)); + } + } catch (Exception e) { + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } } } diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/MessageStatusMigration.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/MessageStatusMigration.java new file mode 100644 index 00000000..a0ffa256 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/MessageStatusMigration.java @@ -0,0 +1,56 @@ +package de.thedevstack.conversationsplus.persistance.db.migrations; + +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; + +/** + */ +public class MessageStatusMigration { + // Following status integers copied from old Message class + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + public static final int STATUS_SEND_CANCELED = 9; // FIXME This bullshit is needed until status is handled more properly + + public static MessageDirection getMessageDirection(int status) { + switch (status) { + case STATUS_SEND: + case STATUS_SEND_DISPLAYED: + case STATUS_SEND_FAILED: + case STATUS_SEND_RECEIVED: + case STATUS_SEND_CANCELED: + case STATUS_UNSEND: + case STATUS_WAITING: + case STATUS_OFFERED: + return MessageDirection.OUT; + default: + return MessageDirection.IN; + } + } + + public static MessageStatus getMessageStatus(int status) { + switch (status) { + case STATUS_UNSEND: + case STATUS_WAITING: + return MessageStatus.WAITING; + case STATUS_RECEIVED: + case STATUS_SEND: + return MessageStatus.TRANSMITTED; + case STATUS_OFFERED: + return MessageStatus.TRANSMITTING; + case STATUS_SEND_FAILED: + return MessageStatus.FAILED; + case STATUS_SEND_DISPLAYED: + return MessageStatus.DISPLAYED; + case STATUS_SEND_RECEIVED: + return MessageStatus.RECEIVED; + default: + return MessageStatus.CANCELED; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java b/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java index 3534ccd2..344e25a2 100644 --- a/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java +++ b/src/main/java/de/thedevstack/conversationsplus/persistance/observers/FileDeletionObserver.java @@ -6,6 +6,8 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; @@ -30,11 +32,10 @@ public class FileDeletionObserver extends FileObserver { for (Conversation conversation : XmppConnectionServiceAccessor.xmppConnectionService.getConversations()) { Message message = conversation.findMessageWithFileAndUuid(uuid); if (message != null) { - message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - final int s = message.getStatus(); - if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { - MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED); - } else { + MessageUtil.setAndSaveStatusForFileDeleted(message); + if (MessageUtil.isIncomingMessage(message) + || (MessageUtil.isOutgoingMessage(message) + && !(MessageStatus.TRANSMITTING == message.getMessageStatus() || MessageStatus.WAITING == message.getMessageStatus()))) { UiUpdateHelper.updateConversationUi(); } return; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java index 0dcfa05e..4dd0a714 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/ExportLogsService.java @@ -92,13 +92,11 @@ public class ExportLogsService extends Service { new File(dir, contactJid.toBareJid().toString() + ".txt"))); } String jid = null; - switch (message.getStatus()) { - case Message.STATUS_RECEIVED: + switch (message.getDirection()) { + case IN: jid = getMessageCounterpart(message); break; - case Message.STATUS_SEND: - case Message.STATUS_SEND_RECEIVED: - case Message.STATUS_SEND_DISPLAYED: + case OUT: jid = accountJid.toBareJid().toString(); break; } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java index c21c90d1..c5ada873 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/NotificationService.java @@ -29,9 +29,12 @@ import java.util.List; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusColors; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.enums.MessageDirection; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; @@ -62,7 +65,7 @@ public class NotificationService { } public boolean notify(final Message message) { - return (message.getStatus() == Message.STATUS_RECEIVED) + return MessageUtil.isIncomingMessage(message) && !message.isRead() && ConversationsPlusPreferences.showNotification() && !message.getConversation().isMuted() @@ -133,25 +136,28 @@ public class NotificationService { } public void push(final Message message) { - if (!notify(message)) { - return; - } - mXmppConnectionService.updateUnreadCountBadge(); - final boolean isScreenOn = mXmppConnectionService.isInteractive(); - if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { - return; - } - synchronized (notifications) { - pushToStack(message); - final Account account = message.getConversation().getAccount(); - final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) - && !account.inGracePeriod() - && !this.inMiniGracePeriod(account); - updateNotification(doNotify); - if (doNotify) { - notifyPebble(message); + if (message.isMamReceived()) { + return; + } + if (notify(message)) { + mXmppConnectionService.updateUnreadCountBadge(); + final boolean isScreenOn = mXmppConnectionService.isInteractive(); + if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { + return; + } + synchronized (notifications) { + pushToStack(message); + final Account account = message.getConversation().getAccount(); + final boolean doNotify = (!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account); + updateNotification(doNotify); + if (doNotify) { + notifyPebble(message); + } } } + } public void clear() { @@ -354,11 +360,9 @@ public class NotificationService { private Message getImage(final Iterable<Message> messages) { for (final Message message : messages) { - if (message.getType() != Message.TYPE_TEXT - && message.getTransferable() == null + if (message.getTransferable() == null && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getFileParams() != null - && message.getFileParams().getHeight() > 0) { // TODO Use FileParams.getMimeType() + && MessageUtil.isAttachedFileAnImage(message)) { // TODO Use FileParams.getMimeType() return message; } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java index bad0415f..2285a29d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -55,6 +55,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.services.avatar.AvatarService; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; @@ -71,6 +73,7 @@ import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; import de.thedevstack.conversationsplus.utils.XmppSendUtil; import de.thedevstack.conversationsplus.services.muc.listener.ConferenceServiceDiscoveryReceived; import de.thedevstack.conversationsplus.xmpp.XepRegistry; +import de.thedevstack.conversationsplus.xmpp.chatmarkers.ChatMarkersXep; import de.thedevstack.conversationsplus.xmpp.disco.ServiceDiscoveryIqPacketGenerator; import de.thedevstack.conversationsplus.xmpp.disco.ServiceDiscoveryIqPacketParser; import de.thedevstack.conversationsplus.xmpp.disco.ServiceDiscoveryXep; @@ -97,8 +100,6 @@ import de.thedevstack.conversationsplus.entities.MucOptions.OnRenameListener; import de.thedevstack.conversationsplus.entities.Presence; import de.thedevstack.conversationsplus.entities.Roster; import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; -import de.thedevstack.conversationsplus.entities.Transferable; -import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.generator.IqGenerator; import de.thedevstack.conversationsplus.generator.MessageGenerator; import de.thedevstack.conversationsplus.generator.PresenceGenerator; @@ -224,7 +225,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getAccount() == account) { Message message = conversation.findUnsentMessageWithUuid(uuid); if (message != null) { - MessageUtil.markMessage(message, Message.STATUS_SEND); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTED); } } } @@ -717,6 +718,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa XepRegistry.add(new ServiceDiscoveryXep()); XepRegistry.add(new PingXep()); XepRegistry.add(new OpenPgpXep()); + XepRegistry.add(new ChatMarkersXep(Settings.CONFIRM_MESSAGE_RECEIVED)); final XmppConnection connection = new XmppConnection(account, this); connection.setOnMessagePacketReceivedListener(this.mMessageParser); @@ -752,10 +754,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.deactivateGracePeriod(); MessagePacket packet = null; final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI - || account.getServerIdentity() != XmppConnection.Identity.SLACK) - && !message.edited(); + || account.getServerIdentity() != XmppConnection.Identity.SLACK); boolean saveInDb = addToConversation; - message.setStatus(Message.STATUS_WAITING); + MessageUtil.setMessageStatus(message, MessageStatus.WAITING); if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { message.getConversation().endOtrIfNeeded(); @@ -763,7 +764,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { - MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.FAILED); } }); } @@ -833,9 +834,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { - message.setStatus(Message.STATUS_UNSEND); + MessageUtil.setMessageStatus(message, MessageStatus.TRANSMITTING); } else { - message.setStatus(Message.STATUS_SEND); + MessageUtil.setMessageStatus(message, MessageStatus.TRANSMITTED); } } } else { @@ -867,9 +868,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (resend) { if (packet != null && addToConversation) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { - MessageUtil.markMessage(message, Message.STATUS_UNSEND); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTING); } else { - MessageUtil.markMessage(message, Message.STATUS_SEND); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTED); } } } else { @@ -879,8 +880,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { if (saveInDb) { databaseBackend.createMessage(message); - } else if (message.edited()) { - databaseBackend.updateMessage(message, message.getEditedId()); } } updateConversationUi(); @@ -1085,11 +1084,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onMessageFound(Message message) { if (!FileBackend.isFileAvailable(message)) { - message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); - final int s = message.getStatus(); - if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) { - MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED); - } + MessageUtil.setAndSaveStatusForFileDeleted(message); } } }); @@ -2017,8 +2012,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa MessagePacket outPacket = mMessageGenerator.generateOtrChat(message); if (outPacket != null) { mMessageGenerator.addDelay(outPacket, message.getTimeSent()); - message.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(message); + MessageUtil.setMessageStatus(message, MessageStatus.TRANSMITTED); + DatabaseBackend.getInstance().updateMessage(message); sendMessagePacket(account, outPacket); } } @@ -2140,32 +2135,28 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void resetSendingToWaiting(Account account) { for (Conversation conversation : getConversations()) { if (conversation.getAccount() == account) { - conversation.findUnsentTextMessages(new Conversation.OnMessageFound() { + conversation.findOugoingTextMessagesInTransmission(new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { - MessageUtil.markMessage(message, Message.STATUS_WAITING); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.WAITING); } }); } } } - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { - if (uuid == null) { - return null; - } - for (Conversation conversation : getConversations()) { - if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { - final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); - if (message != null) { - MessageUtil.markMessage(message, status); - } - return message; - } - } - return null; - } + public Message getMessage(final Account account, final Jid recipient, final String uuid) { + if (uuid == null) { + return null; + } + for (Conversation conversation : getConversations()) { + if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { + return conversation.findSentMessageWithUuidOrRemoteId(uuid); + } + } + return null; + } public int unreadCount() { int count = 0; @@ -2258,7 +2249,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void run() { for (Message message : readMessages) { - databaseBackend.updateMessage(message); + DatabaseBackend.getInstance().updateMessage(message); } } }; @@ -2414,11 +2405,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return this.mNotificationService; } - public void resendFailedMessages(final Message message) { - if (message.getStatus() == Message.STATUS_SEND_FAILED - || message.getStatus() == Message.STATUS_SEND_CANCELED) { + public void resendFailedOrCanceledMessages(final Message message) { + if (MessageUtil.isOutgoingMessage(message) + && (MessageStatus.FAILED == message.getMessageStatus() + || MessageStatus.CANCELED == message.getMessageStatus())) { message.setTime(System.currentTimeMillis()); - MessageUtil.markMessage(message, Message.STATUS_WAITING); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.WAITING); this.resendMessage(message, false); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/avatar/AvatarCache.java b/src/main/java/de/thedevstack/conversationsplus/services/avatar/AvatarCache.java index a4def149..5e10f27c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/avatar/AvatarCache.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/avatar/AvatarCache.java @@ -12,8 +12,10 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.ListItem; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.MucOptions; +import de.thedevstack.conversationsplus.enums.MessageDirection; import de.thedevstack.conversationsplus.utils.AvatarUtil; import de.thedevstack.conversationsplus.utils.ImageUtil; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; /** @@ -177,7 +179,7 @@ public final class AvatarCache { public static Bitmap get(Message message, int size, boolean cachedOnly) { final Conversation conversation = message.getConversation(); - if (message.getStatus() == Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { Contact c = message.getContact(); if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) { return get(c, size, cachedOnly); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java index 63f1547f..62fed29b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java @@ -9,6 +9,7 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.FileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpUploadFileTransferEntity; import de.thedevstack.conversationsplus.utils.MessageUtil; @@ -124,9 +125,8 @@ public class FileTransferManager implements FileTransferStatusListener { } } } - if (!retransferStarted) { - entity.getMessage().getFileParams().setFileStatus(FileStatus.UPLOAD_FAILED); - MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_FAILED); + if (!retransferStarted && (entity instanceof HttpUploadFileTransferEntity)) { + MessageUtil.setAndSaveFileStatus(entity.getMessage(), FileStatus.UPLOAD_FAILED); } } } @@ -134,12 +134,13 @@ public class FileTransferManager implements FileTransferStatusListener { @Override public void onCancel(FileTransferEntity entity) { this.activeTransfers.remove(entity.getMessage().getUuid()); - MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_CANCELED); + MessageUtil.setAndSaveMessageStatus(entity.getMessage(), MessageStatus.CANCELED); } @Override public void onSuccess(FileTransferEntity entity) { this.activeTransfers.remove(entity.getMessage().getUuid()); + MessageUtil.setAndSaveMessageStatus(entity.getMessage(), MessageStatus.TRANSMITTED); } private boolean startFileTransfer(Message message, boolean delayed, WeightedTransferService wts) { diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java index 5885e2e1..4401fb06 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java @@ -12,20 +12,12 @@ import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; * */ public class AutomaticFileDownload implements HttpHeadRetrievedListener { - private boolean notify; - - public AutomaticFileDownload(boolean notify) { - this.notify = notify; - } @Override public void onFileSizeRetrieved(long size, Message message) { if (!this.transferFile(message)) { } - if (this.notify) { - XmppConnectionServiceAccessor.xmppConnectionService.getNotificationService().push(message); - } } /** diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java index 1313bcca..62c1af59 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java @@ -10,6 +10,7 @@ import de.thedevstack.conversationsplus.http.Http; import de.thedevstack.conversationsplus.http.HttpClient; import de.thedevstack.conversationsplus.http.HttpHeadRetrievedListener; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; +import de.thedevstack.conversationsplus.utils.MessageParserUtil; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import okhttp3.Call; @@ -36,12 +37,11 @@ public class HttpRetrieveHead implements Http, Callback { * If this code is reached and the URL is null something went wrong. * Try again to extract the file parameters from the message. */ - MessageUtil.extractFileParamsFromBody(message); + MessageParserUtil.extractFileParamsFromBody(message); this.url = (null != message.getFileParams()) ? message.getFileParams().getUrl() : null; if (null == this.url) { - message.setTreatAsDownloadable(Message.Decision.NEVER); // TODO find sth better if (null != message.getFileParams()) { - MessageUtil.setAndSaveFileStatus(message, FileStatus.UNDEFINED); + MessageUtil.setAndSaveFileStatus(message, FileStatus.NOT_FOUND); } } } @@ -89,7 +89,7 @@ public class HttpRetrieveHead implements Http, Callback { fileParams.setSize(size); fileParams.setMimeType(contentType); if (0 < size) { - fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); + MessageUtil.setFileStatus(message, FileStatus.NEEDS_DOWNLOAD); } DatabaseBackend.getInstance().updateMessage(message); UiUpdateHelper.updateConversationUi(); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java index 54186983..19c965e2 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java @@ -32,7 +32,7 @@ public class HttpUploadFileTransferEntity extends FileTransferEntity { fileParams = new FileParams(); this.getMessage().setFileParams(fileParams); } - fileParams.setFileStatus(FileStatus.NEEDS_UPLOAD); + MessageUtil.setFileStatus(this.getMessage(), FileStatus.NEEDS_DOWNLOAD); if (Config.ENCRYPT_ON_HTTP_UPLOADED || message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_OTR) { @@ -96,7 +96,7 @@ public class HttpUploadFileTransferEntity extends FileTransferEntity { getUrl = new URL(getUrl.toString() + "#" + CryptoHelper.bytesToHex(this.getKey())); } - this.getMessage().getFileParams().setFileStatus(FileStatus.UPLOADED); + MessageUtil.setFileStatus(getMessage(), FileStatus.UPLOADED); MessageUtil.updateFileParams(this.getMessage(), getUrl); } catch (MalformedURLException e) { Logging.e("httpupload", "Not a valid get url"); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java index 28bb3a1c..b901ae4b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java @@ -9,6 +9,7 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.FileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService; @@ -62,7 +63,7 @@ public class HttpUploadFileTransferService extends AbstractFileTransferService i Logging.d("httpupload", "Requesting upload slot for file upload"); IqPacket request = HttpUploadRequestSlotPacketGenerator.generate(account, message.getContact().getJid(), file.getName(), file.getSize(), file.getMimeType()); XmppSendUtil.sendIqPacket(account, request, new HttpUploadSlotRequestReceived(entity)); - MessageUtil.markMessage(message, Message.STATUS_UNSEND); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTING); Logging.d("httpupload", "Upload slot for file upload requested"); started = true; diff --git a/src/main/java/de/thedevstack/conversationsplus/services/mam/MessageArchiveService.java b/src/main/java/de/thedevstack/conversationsplus/services/mam/MessageArchiveService.java index 6ad413b4..34620808 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/mam/MessageArchiveService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/mam/MessageArchiveService.java @@ -15,6 +15,7 @@ import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.generator.AbstractGenerator; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.OnAdvancedStreamFeaturesLoaded; @@ -47,7 +48,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } } - Pair<Long,String> pair = mXmppConnectionService.databaseBackend.getLastMessageReceived(account); + Pair<Long,String> pair = DatabaseBackend.getInstance().getLastMessageReceived(account); long startCatchup = pair == null ? 0 : pair.first; long endCatchup = account.getXmppConnection().getLastSessionEstablished(); final Query query; @@ -168,14 +169,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } final Conversation conversation = query.getConversation(); if (conversation != null) { - conversation.sort(); conversation.setHasMessagesLeftOnServer(!done); - } else { - for(Conversation tmp : this.mXmppConnectionService.getConversations()) { - if (tmp.getAccount() == query.getAccount()) { - tmp.sort(); - } - } } if (query.hasCallback()) { query.callback(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index cd2ee4f8..4cecd7f8 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -43,6 +43,8 @@ import java.util.Collections; import java.util.List; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService; import de.thedevstack.conversationsplus.ui.dialogs.SimpleConfirmDialog; @@ -124,8 +126,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private RelativeLayout snackbar; private TextView snackbarMessage; private TextView snackbarAction; - private boolean messagesLoaded = true; - private Toast messageLoaderToast; private final int KEYCHAIN_UNLOCK_NOT_REQUIRED = 0; private final int KEYCHAIN_UNLOCK_REQUIRED = 1; private final int KEYCHAIN_UNLOCK_PENDING = 2; @@ -242,16 +242,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private ConversationActivity activity; private Message selectedMessage; - public void setMessagesLoaded() { - this.messagesLoaded = true; - } - private void sendMessage() { final String body = mEditMessage.getText().toString(); if (body.length() == 0 || this.conversation == null) { return; } - Message message = new Message(conversation, body, conversation.getNextEncryption()); + Message message = MessageUtil.createOutgoingMessage(conversation, body); + if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); @@ -503,8 +500,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) { shareWith.setVisible(true); } - if (m.getStatus() == Message.STATUS_SEND_FAILED - || Message.STATUS_SEND_CANCELED == m.getStatus()) { + if (MessageUtil.isOutgoingMessage(m) + && (MessageStatus.FAILED == m.getMessageStatus() + || MessageStatus.CANCELED == m.getMessageStatus())) { sendAgain.setVisible(true); } if (m.hasFileOnRemoteHost() @@ -518,15 +516,17 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa downloadFile.setTitle(activity.getString(R.string.download_x_file,UIHelper.getFileDescriptionString(activity, m))); } if ((t != null && !(t instanceof TransferablePlaceholder)) - || (m.isFileOrImage() && (m.getStatus() == Message.STATUS_WAITING - || m.getStatus() == Message.STATUS_OFFERED))) { + || (m.isFileOrImage() && (MessageStatus.WAITING == m.getMessageStatus() || MessageStatus.TRANSMITTING == m.getMessageStatus()))) { // TODO Welche Einschraenkung noch um uebertragung abzubrechen? cancelTransmission.setVisible(true); } if (treatAsFile) { deleteFile.setVisible(true); deleteFile.setTitle(activity.getString(R.string.delete_x_file,UIHelper.getFileDescriptionString(activity, m))); } - if (m.isHttpUploaded() && MessageUtil.isMessageSent(m) && AccountUtil.isFileTransferHttpAvailable(m.getConversation().getAccount())) { + if (m.isHttpUploaded() + && AccountUtil.isFileTransferHttpAvailable(m.getConversation().getAccount()) + && MessageUtil.isOutgoingMessage(m) + && (MessageUtil.isMessageTransmittedOrDisplayedOrReceived(m) || MessageStatus.FAILED == m.getMessageStatus())) { MenuItem deleteRemoteFile = menu.findItem(R.id.msg_ctx_menu_delete_remote_file); deleteRemoteFile.setVisible(true); } @@ -613,7 +613,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa return; } } - activity.xmppConnectionService.resendFailedMessages(message); + activity.xmppConnectionService.resendFailedOrCanceledMessages(message); } private void copyUrl(Message message) { @@ -651,7 +651,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (transferable != null) { transferable.cancel(); } else { - MessageUtil.markMessage(message, Message.STATUS_SEND_CANCELED); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.CANCELED); } } @@ -726,7 +726,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mEditMessage.setKeyboardListener(this); this.messagesView.setAdapter(messageListAdapter); updateMessages(); - this.messagesLoaded = true; int size = this.messageList.size(); if (size > 0) { messagesView.setSelection(size - 1); @@ -878,7 +877,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private Message getLastPgpDecryptableMessage() { for (final Message message : this.messageList) { if (message.getEncryption() == Message.ENCRYPTION_PGP - && (message.getStatus() == Message.STATUS_RECEIVED || message.getStatus() >= Message.STATUS_SEND) + && MessageUtil.isMessageTransmittedOrDisplayedOrReceived(message) && message.getTransferable() == null) { return message; } @@ -1047,18 +1046,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa synchronized (this.messageList) { if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); - if (state == ChatState.COMPOSING) { - //this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); - } else if (state == ChatState.PAUSED) { - //this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); - } else { + if (state != ChatState.COMPOSING && state != ChatState.PAUSED) { // Do not show status message for (int i = this.messageList.size() - 1; i >= 0; --i) { - if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { + Message message = this.messageList.get(i); + if (MessageUtil.isIncomingMessage(message)) { return; } else { - if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + if (MessageStatus.DISPLAYED == message.getMessageStatus()) { this.messageList.add(i + 1, - Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + MessageUtil.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); return; } } @@ -1315,7 +1311,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updatePgpMessages(); } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { final String body = mEditMessage.getText().toString(); - Message message = new Message(conversation, body, conversation.getNextEncryption()); + Message message = MessageUtil.createOutgoingMessage(conversation, body); sendAxolotlMessage(message); } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java index 8852212b..080c7a51 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ShareWithActivity.java @@ -32,6 +32,7 @@ import de.thedevstack.conversationsplus.ui.adapter.ConversationAdapter; import de.thedevstack.conversationsplus.utils.AccountUtil; import de.thedevstack.conversationsplus.utils.ConversationUtil; import de.thedevstack.conversationsplus.utils.FileUtils; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -319,10 +320,8 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer final OnPresenceSelected callback = new OnPresenceSelected() { @Override public void onPresenceSelected() { - Message message = new Message(conversation,share.text, conversation.getNextEncryption()); - if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { - message.setCounterpart(conversation.getNextCounterpart()); - } + Message message = MessageUtil.createOutgoingMessage(conversation, share.text); + xmppConnectionService.sendMessage(message); replaceToast(getString(R.string.shared_text_with_x, conversation.getName())); finish(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java index e87cb052..97219b18 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java @@ -101,7 +101,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { imagePreview.setVisibility(View.GONE); CharSequence msgText = preview.first; String msgPrefix = null; - if (MessageUtil.isMessageSent(message)) { + if (MessageUtil.isOutgoingMessage(message)) { msgPrefix = activity.getString(R.string.cplus_me); } else if (conversation.getMode() == Conversation.MODE_MULTI) { msgPrefix = UIHelper.getMessageDisplayName(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java index 7c7a6b57..331f8458 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java @@ -14,10 +14,7 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import java.util.List; @@ -32,6 +29,7 @@ import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.avatar.AvatarCache; import de.thedevstack.conversationsplus.services.avatar.AvatarService; import de.thedevstack.conversationsplus.services.filetransfer.http.download.AutomaticFileDownload; @@ -48,12 +46,12 @@ import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; import de.thedevstack.conversationsplus.utils.ui.ViewUtil; -public class MessageAdapter extends ArrayAdapter<Message> { +import static de.thedevstack.conversationsplus.ui.adapter.MessageViewHolder.ME_COMMAND; +import static de.thedevstack.conversationsplus.ui.adapter.MessageViewHolder.RECEIVED; +import static de.thedevstack.conversationsplus.ui.adapter.MessageViewHolder.SENT; +import static de.thedevstack.conversationsplus.ui.adapter.MessageViewHolder.STATUS; - private static final int SENT = 0; - private static final int RECEIVED = 1; - private static final int STATUS = 2; - private static final int ME_COMMAND = 3; +public class MessageAdapter extends ArrayAdapter<Message> { private ConversationActivity activity; @@ -81,7 +79,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { return ME_COMMAND; } else if (message.getType() == Message.TYPE_STATUS) { return STATUS; - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { + } else if (MessageUtil.isIncomingMessage(message)) { return RECEIVED; } @@ -101,15 +99,15 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayStatus(ViewHolder viewHolder, Message message, int type) { + private void displayStatus(MessageViewHolder viewHolder, Message message, int type) { String filesize = null; String info = null; boolean error = false; ViewUtil.gone(viewHolder.indicatorReceived); boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI - && message.getStatus() <= Message.STATUS_RECEIVED; - if (message.hasFileAttached() || message.getTransferable() != null) { + && MessageUtil.isIncomingMessage(message); + if (message.hasFileAttached() || MessageUtil.hasDownloadableLink(message)) { FileParams params = message.getFileParams(); if (null != params) { filesize = UIHelper.getHumanReadableFileSize(params.getSize()); @@ -119,36 +117,45 @@ public class MessageAdapter extends ArrayAdapter<Message> { error = true; } } - switch (message.getStatus()) { - case Message.STATUS_WAITING: + Transferable d = message.getTransferable(); + switch (message.getMessageStatus()) { + case WAITING: info = getContext().getString(R.string.waiting); break; - case Message.STATUS_UNSEND: - Transferable d = message.getTransferable(); - if (d != null) { - info = getContext().getString(R.string.sending_file, d.getProgress()); + case TRANSMITTING: + if (null != d) { + int resId = R.string.receiving_file; + if (MessageUtil.isOutgoingMessage(message)) { + resId = R.string.sending_file; + } + info = getContext().getString(resId, d.getProgress()); + } else if (null != message.getFileParams() && FileStatus.NEEDS_UPLOAD == message.getFileParams().getFileStatus()) { // FIXME: Remove when Jingle is migrated + info = getContext().getString(R.string.offering); } else { - info = getContext().getString(R.string.sending); + if (!MessageUtil.needsDownload(message)) { + int resId = R.string.receiving; + if (MessageUtil.isOutgoingMessage(message)) { + resId = R.string.sending; + } + info = getContext().getString(resId); + } } break; - case Message.STATUS_OFFERED: - info = getContext().getString(R.string.offering); - break; - case Message.STATUS_SEND_RECEIVED: + case RECEIVED: if (ConversationsPlusPreferences.indicateReceived()) { viewHolder.indicatorReceived.setVisibility(View.VISIBLE); } break; - case Message.STATUS_SEND_DISPLAYED: + case DISPLAYED: if (ConversationsPlusPreferences.indicateReceived()) { viewHolder.indicatorReceived.setVisibility(View.VISIBLE); } break; - case Message.STATUS_SEND_FAILED: + case FAILED: info = getContext().getString(R.string.send_failed); error = true; break; - case Message.STATUS_SEND_CANCELED: + case CANCELED: info = getContext().getString(R.string.send_canceled); error = true; break; @@ -156,7 +163,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (multiReceived) { info = UIHelper.getMessageDisplayName(message); } - break; } this.displayEncryptionIndicator(message, viewHolder); @@ -166,7 +172,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { this.displayRemoteFileStatus(message, viewHolder); } - private void displayRemoteFileStatus(Message message, ViewHolder viewHolder) { + private void displayRemoteFileStatus(Message message, MessageViewHolder viewHolder) { if (message.hasFileAttached() && null != message.getFileParams() && null != viewHolder.remoteFileStatus) { FileStatus fileStatus = message.getFileParams().getFileStatus(); if (fileStatus == FileStatus.DELETE_FAILED || fileStatus == FileStatus.DELETED || fileStatus == FileStatus.DELETING || fileStatus == FileStatus.NOT_FOUND) { @@ -193,7 +199,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayMessageTime(Message message, ViewHolder viewHolder, String filesize, String info, boolean error, boolean isSent) { + private void displayMessageTime(Message message, MessageViewHolder viewHolder, String filesize, String info, boolean error, boolean isSent) { if (error && isSent) { viewHolder.time.setTextColor(ConversationsPlusColors.warning()); } else { @@ -201,7 +207,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getTimeSent()); String timeText = null; - if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { StringBuilder timeTextBuilder = new StringBuilder(); timeTextBuilder.append((null != formatedTime) ? formatedTime + ((null != info || null != filesize) ? " \u00B7 " : "") : ""); timeTextBuilder.append((null != filesize) ? filesize + ((null != info) ? " \u00B7 " : "") : ""); @@ -227,7 +233,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { TextViewUtil.setTextWithoutAutoLink(viewHolder.time, timeText); } - private void displayEncryptionIndicator(Message message, ViewHolder viewHolder) { + private void displayEncryptionIndicator(Message message, MessageViewHolder viewHolder) { if (message.getEncryption() == Message.ENCRYPTION_NONE) { viewHolder.indicator.setVisibility(View.GONE); } else { @@ -260,40 +266,28 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayInfoMessage(ViewHolder viewHolder, String text) { - if (viewHolder.download_button != null) { - viewHolder.download_button.setVisibility(View.GONE); - } - if (null != viewHolder.image) { - viewHolder.image.setVisibility(View.GONE); - } + private void displayInfoMessage(MessageViewHolder viewHolder, String text) { + ViewUtil.gone(viewHolder.download_button, viewHolder.image); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(text); + TextViewUtil.showAndSetText(viewHolder.messageBody, text); viewHolder.messageBody.setTextColor(getMessageTextColor(viewHolder.darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayDecryptionFailed(ViewHolder viewHolder) { - if (viewHolder.download_button != null) { - viewHolder.download_button.setVisibility(View.GONE); - } - - viewHolder.image.setVisibility(View.GONE); + private void displayDecryptionFailed(MessageViewHolder viewHolder) { + ViewUtil.gone(viewHolder.download_button, viewHolder.image); - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText(getContext().getString( - R.string.decryption_failed)); + TextViewUtil.showAndSetText(viewHolder.messageBody, R.string.decryption_failed); viewHolder.messageBody.setTextColor(getMessageTextColor(viewHolder.darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayTextMessage(final ViewHolder viewHolder, final Message message) { + private void displayTextMessage(final MessageViewHolder viewHolder, final Message message) { ViewUtil.gone(viewHolder.download_button, viewHolder.image); - viewHolder.messageBody.setVisibility(View.VISIBLE); + ViewUtil.visible(viewHolder.messageBody); viewHolder.messageBody.setIncludeFontPadding(true); if (message.getBody() != null) { @@ -317,7 +311,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } else { String privateMarker; - if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { privateMarker = activity.getString(R.string.private_message); } else { final String to; @@ -352,15 +346,14 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setOnLongClickListener(openContextMenu); } - private void displayDownloadButton(ViewHolder viewHolder, String btnText, OnClickListener onClickListener) { - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(btnText); - viewHolder.download_button.setOnClickListener(onClickListener); + private void displayDownloadButton(MessageViewHolder viewHolder, String btnText, OnClickListener onClickListener) { + TextViewUtil.showAndSetText(viewHolder.download_button, btnText) + .setOnClickListener(onClickListener); viewHolder.download_button.setOnLongClickListener(openContextMenu); } - private void displayFileInfoForFileMessage(final Message message, ViewHolder viewHolder) { - viewHolder.messageBody.setVisibility(View.VISIBLE); + private void displayFileInfoForFileMessage(final Message message, MessageViewHolder viewHolder) { + ViewUtil.visible(viewHolder.messageBody); StringBuilder fileInfos = new StringBuilder(); String filename = UIHelper.getDisplayFilename(message); fileInfos.append((null != filename && !filename.isEmpty()) ? (filename) : ""); @@ -368,8 +361,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { TextViewUtil.setTextWithoutAutoLink(viewHolder.messageBody, fileInfos); } - private void displayDownloadableMessage(ViewHolder viewHolder, final Message message) { - viewHolder.image.setVisibility(View.GONE); + private void displayDownloadableMessage(MessageViewHolder viewHolder, final Message message) { + ViewUtil.gone(viewHolder.image); FileParams fileParams = message.getFileParams(); String btnText; int resId = R.string.download_x_file; @@ -393,8 +386,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { }); } - private void displayOpenableMessage(ViewHolder viewHolder, final Message message) { - viewHolder.image.setVisibility(View.GONE); + private void displayOpenableMessage(MessageViewHolder viewHolder, final Message message) { + ViewUtil.gone(viewHolder.image); FileParams fileParams = message.getFileParams(); String btnText; @@ -410,19 +403,18 @@ public class MessageAdapter extends ArrayAdapter<Message> { } - private void displayLocationMessage(ViewHolder viewHolder, final Message message) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setText(R.string.show_location); - viewHolder.download_button.setOnClickListener(new OpenLocationOnClickListener(this.activity, message)); + private void displayLocationMessage(MessageViewHolder viewHolder, final Message message) { + ViewUtil.gone(viewHolder.image, viewHolder.messageBody); + + TextViewUtil.showAndSetText(viewHolder.download_button, R.string.show_location) + .setOnClickListener(new OpenLocationOnClickListener(this.activity, message)); viewHolder.download_button.setOnLongClickListener(openContextMenu); } - private void displayImageMessage(ViewHolder viewHolder, final Message message) { - ViewUtil.gone(viewHolder.download_button); - + private void displayImageMessage(MessageViewHolder viewHolder, final Message message) { + ViewUtil.gone(viewHolder.download_button, viewHolder.messageBody); + ViewUtil.visible(viewHolder.image); ImageUtil.loadBitmap(message, viewHolder.image, viewHolder.messageBody, true); viewHolder.image.setOnClickListener(new OpenFileOnClickListener(this.activity, message)); @@ -430,11 +422,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.image.setOnLongClickListener(openContextMenu); } - private View displayStatusMessage(final Message message, ViewHolder viewHolder) { + private View displayStatusMessage(final Message message, MessageViewHolder viewHolder) { final Conversation conversation = message.getConversation(); - viewHolder.status_message.setVisibility(View.VISIBLE); - viewHolder.contact_picture.setVisibility(View.VISIBLE); + ViewUtil.visible(viewHolder.status_message, viewHolder.contact_picture); + if (conversation.getMode() == Conversation.MODE_SINGLE) { viewHolder.contact_picture.setImageBitmap(AvatarCache.get(conversation.getContact(), activity.getPixel(32))); @@ -445,29 +437,20 @@ public class MessageAdapter extends ArrayAdapter<Message> { return viewHolder.view; } - private void displayFileMessage(final Message message, ViewHolder viewHolder) { + private void displayFileMessage(final Message message, MessageViewHolder viewHolder) { if (!(message.trusted() && MessageUtil.needsDownload(message) && ConversationsPlusPreferences.autoAcceptFileSize() > 0 && message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { - new AutomaticFileDownload(false).transferFile(message); + new AutomaticFileDownload().transferFile(message); } Transferable transferable = message.getTransferable(); if (FileStatus.CHECKING_FILE_SIZE == message.getFileParams().getFileStatus()) { displayInfoMessage(viewHolder, activity.getString(R.string.checking_remote_filesize)); } else if (MessageUtil.isAttachedFileAnImage(message) - && (FileStatus.DOWNLOADED == message.getFileParams().getFileStatus() - || FileStatus.DELETED == message.getFileParams().getFileStatus() - || FileStatus.DELETING == message.getFileParams().getFileStatus() - || FileStatus.DELETE_FAILED == message.getFileParams().getFileStatus() - || FileStatus.NEEDS_UPLOAD == message.getFileParams().getFileStatus() - || FileStatus.UPLOADED == message.getFileParams().getFileStatus() - || FileStatus.UPLOAD_FAILED == message.getFileParams().getFileStatus() - || (null != transferable - && (transferable.isCanceled() - || Transferable.STATUS_UPLOADING == transferable.getStatus())))) { + && hasFileStatusToShowMessage(message)) { displayImageMessage(viewHolder, message); - } else if ((MessageUtil.isTypeFileAndDecrypted(message) || FileStatus.DOWNLOADED == message.getFileParams().getFileStatus()) + } else if ((MessageUtil.isTypeFileAndDecrypted(message) || hasFileStatusToShowMessage(message)) && !MessageUtil.needsDownload(message)) { displayOpenableMessage(viewHolder, message); } else if (Message.Decision.NEVER == message.treatAsDownloadable() || !MessageUtil.mayFileRemoteAvailable(message)) { @@ -478,10 +461,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { case Transferable.STATUS_OFFER_CHECK_FILESIZE: displayDownloadableMessage(viewHolder, message); break; - case Transferable.STATUS_UPLOADING: - // Should not happen, since this is now covered by the other if-statements - // TODO Maybe in Jingle File Transfer?? Needs to be checked! - break; case Transferable.STATUS_DELETED: case Transferable.STATUS_CHECKING: case Transferable.STATUS_FAILED: @@ -495,7 +474,21 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayAvatar(final Message message, int type, ViewHolder viewHolder) { + private boolean hasFileStatusToShowMessage(Message message) { + Transferable transferable = message.getTransferable(); + return (FileStatus.DOWNLOADED == message.getFileParams().getFileStatus() + || FileStatus.DELETED == message.getFileParams().getFileStatus() + || FileStatus.DELETING == message.getFileParams().getFileStatus() + || FileStatus.DELETE_FAILED == message.getFileParams().getFileStatus() + || FileStatus.NEEDS_UPLOAD == message.getFileParams().getFileStatus() + || FileStatus.UPLOADED == message.getFileParams().getFileStatus() + || FileStatus.UPLOAD_FAILED == message.getFileParams().getFileStatus() + || (null != transferable + && (transferable.isCanceled() + || Transferable.STATUS_UPLOADING == transferable.getStatus()))); + } + + private void displayAvatar(final Message message, int type, MessageViewHolder viewHolder) { if (type == ME_COMMAND || (type == RECEIVED)) { // && message.getConversation().getMode() == Conversation.MODE_MULTI ImageView imageView = viewHolder.contact_picture; if (null != imageView) { @@ -509,79 +502,24 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private View initializeView(int type, ViewGroup parent) { - Integer viewResId = null; - - switch (type) { - case SENT: - viewResId = R.layout.message_sent; - break; - case RECEIVED: - viewResId = R.layout.message_received; - break; - case STATUS: - viewResId = R.layout.message_status; - break; - case ME_COMMAND: - viewResId = R.layout.message_mecmd; - break; - } - - return activity.getLayoutInflater().inflate(viewResId, parent, false); - } - - private ViewHolder initializeViewHolderAndView(int type, ViewGroup parent) { - View view = initializeView(type, parent); - ViewHolder viewHolder = new ViewHolder(view); - if (SENT == type - || RECEIVED == type - || ME_COMMAND == type) { - viewHolder.message_box = ViewUtil.visible(view, R.id.message_box); - viewHolder.indicator = (ImageView) view.findViewById(R.id.security_indicator); - viewHolder.messageBody = (TextView) view.findViewById(R.id.message_body); - viewHolder.time = (TextView) view.findViewById(R.id.message_time); - viewHolder.indicatorReceived = (ImageView) view.findViewById(R.id.indicator_received); - } - if ((SENT == type - || RECEIVED == type)) { - viewHolder.download_button = (Button) view.findViewById(R.id.download_button); - viewHolder.image = (ImageView) view.findViewById(R.id.message_image); - } - if (ME_COMMAND == type - || (RECEIVED == type)) { // && message.getConversation().getMode() == Conversation.MODE_MULTI --> only muc received msgs - viewHolder.contact_picture = ViewUtil.visible(view, R.id.message_photo); - } - if (RECEIVED == type) { - viewHolder.encryption = (TextView) view.findViewById(R.id.message_encryption); - } - if (STATUS == type) { - viewHolder.contact_picture = ViewUtil.visible(view, R.id.message_photo); - viewHolder.status_message = TextViewUtil.visible(view, R.id.status_message); - } - if (SENT == type) { // This field is only useful for sent messages -> because of deletion of own files -> maybe a use case for recvd possible - viewHolder.remoteFileStatus = TextViewUtil.gone(view, R.id.remote_file_status); - } - view.setTag(viewHolder); - - return viewHolder; - } - @Override public View getView(int position, View view, @NonNull ViewGroup parent) { final Message message = getItem(position); - if (null == message) { + if (null == message + || MessageStatus.DISPLAYED == message.getMessageStatus() + || MessageStatus.RECEIVED == message.getMessageStatus()) { return view; } final boolean isInValidSession = message.isValidInSession(); final Conversation conversation = message.getConversation(); final Account account = conversation.getAccount(); final int type = getItemViewType(position); - ViewHolder viewHolder; + MessageViewHolder viewHolder; if (null == view) { - viewHolder = initializeViewHolderAndView(type, parent); + viewHolder = new MessageViewHolder(this.activity, type, parent); view = viewHolder.view; } else { - viewHolder = (ViewHolder) view.getTag(); + viewHolder = (MessageViewHolder) view.getTag(); if (null == viewHolder) { return view; } @@ -594,7 +532,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { this.displayAvatar(message, type, viewHolder); viewHolder.darkBackground = (type == RECEIVED && !isInValidSession); - this.displayStatus(viewHolder, message, type); if (null != message.getTransferable() || message.hasFileAttached() || MessageUtil.hasDownloadableLink(message)) { displayFileMessage(message, viewHolder); @@ -607,21 +544,21 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else { displayTextMessage(viewHolder, message); } + this.displayStatus(viewHolder, message, type); if (type == RECEIVED) { if (isInValidSession) { - viewHolder.encryption.setVisibility(View.GONE); + ViewUtil.gone(viewHolder.encryption); } else { viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning); - viewHolder.encryption.setVisibility(View.VISIBLE); - viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); + TextViewUtil.showAndSetText(viewHolder.encryption, CryptoHelper.encryptionTypeToText(message.getEncryption())); } } return view; } - private void displayPgpEncryptedMessage(ViewHolder viewHolder, Account account) { + private void displayPgpEncryptedMessage(MessageViewHolder viewHolder, Account account) { if (activity.hasPgp()) { if (account.getPgpDecryptionService().isRunning()) { displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting)); @@ -638,25 +575,4 @@ public class MessageAdapter extends ArrayAdapter<Message> { }); } } - - private static class ViewHolder { - protected ViewHolder(View view) { - this.view = view; - } - - protected View view; - - protected LinearLayout message_box; - protected Button download_button; - protected ImageView image; - protected ImageView indicator; - protected ImageView indicatorReceived; - protected TextView time; - protected TextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - public TextView remoteFileStatus; - protected boolean darkBackground; - } }
\ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageViewHolder.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageViewHolder.java new file mode 100644 index 00000000..d66d4f01 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageViewHolder.java @@ -0,0 +1,97 @@ +package de.thedevstack.conversationsplus.ui.adapter; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; +import de.thedevstack.conversationsplus.utils.ui.ViewUtil; + +/** + */ +class MessageViewHolder { + + static final int SENT = 0; + static final int RECEIVED = 1; + static final int STATUS = 2; + static final int ME_COMMAND = 3; + + View view; + + LinearLayout message_box; + Button download_button; + ImageView image; + ImageView indicator; + ImageView indicatorReceived; + TextView time; + TextView messageBody; + ImageView contact_picture; + TextView status_message; + TextView encryption; + TextView remoteFileStatus; + boolean darkBackground; + + MessageViewHolder(Activity activity, int type, ViewGroup parent) { + int viewResId = this.resolveViewId(type, parent); + this.view = activity.getLayoutInflater().inflate(viewResId, parent, false); + this.initializeViewElements(type); + } + + private int resolveViewId(int type, ViewGroup parent) { + Integer viewResId = null; + + switch (type) { + case SENT: + viewResId = R.layout.message_sent; + break; + case RECEIVED: + viewResId = R.layout.message_received; + break; + case STATUS: + viewResId = R.layout.message_status; + break; + case ME_COMMAND: + viewResId = R.layout.message_mecmd; + break; + } + + return viewResId; + } + + private void initializeViewElements(int type) { + if (SENT == type + || RECEIVED == type + || ME_COMMAND == type) { + this.message_box = ViewUtil.visible(view, R.id.message_box); + this.indicator = (ImageView) view.findViewById(R.id.security_indicator); + this.messageBody = (TextView) view.findViewById(R.id.message_body); + this.time = (TextView) view.findViewById(R.id.message_time); + this.indicatorReceived = (ImageView) view.findViewById(R.id.indicator_received); + } + if ((SENT == type + || RECEIVED == type)) { + this.download_button = (Button) view.findViewById(R.id.download_button); + this.image = (ImageView) view.findViewById(R.id.message_image); + } + if (ME_COMMAND == type + || (RECEIVED == type)) { // && message.getConversation().getMode() == Conversation.MODE_MULTI --> only muc received msgs + this.contact_picture = ViewUtil.visible(view, R.id.message_photo); + } + if (RECEIVED == type) { + this.encryption = (TextView) view.findViewById(R.id.message_encryption); + } + if (STATUS == type) { + this.contact_picture = ViewUtil.visible(view, R.id.message_photo); + this.status_message = TextViewUtil.visible(view, R.id.status_message); + } + if (SENT == type) { // This field is only useful for sent messages -> because of deletion of own files -> maybe a use case for recvd possible + this.remoteFileStatus = TextViewUtil.gone(view, R.id.remote_file_status); + } + view.setTag(this); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java index b43384a4..60431d54 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java @@ -13,6 +13,10 @@ import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; @@ -78,26 +82,30 @@ public class MessageDetailsDialog extends AbstractAlertDialog { protected void displayMessageStatusInfo(View view, Message message) { TextView msgStatusTextView = (TextView) view.findViewById(R.id.dlgMsgDetMsgStatus); int msgStatusResId; - switch (message.getStatus()) { - case Message.STATUS_WAITING: + switch (message.getMessageStatus()) { + case WAITING: msgStatusResId = R.string.dlg_msg_details_msg_status_waiting; break; - case Message.STATUS_UNSEND: - msgStatusResId = R.string.dlg_msg_details_msg_status_unsend; + case TRANSMITTING: + if (MessageUtil.isOutgoingMessage(message)) { + msgStatusResId = R.string.dlg_msg_details_msg_status_unsend; + } else if (FileStatus.NEEDS_UPLOAD == message.getFileParams().getFileStatus()) { // FIXME: Remove when Jingle is migrated + msgStatusResId = R.string.dlg_msg_details_msg_status_offered; + } else { + msgStatusResId = R.string.dlg_msg_details_msg_status_receiving; + } break; - case Message.STATUS_OFFERED: - msgStatusResId = R.string.dlg_msg_details_msg_status_offered; - break; - case Message.STATUS_SEND_FAILED: + case FAILED: msgStatusResId = R.string.dlg_msg_details_msg_status_failed; msgStatusTextView.setTextColor(ConversationsPlusColors.error()); break; - case Message.STATUS_RECEIVED: - msgStatusResId = R.string.dlg_msg_details_msg_status_received; + case TRANSMITTED: + if (MessageUtil.isIncomingMessage(message)) { + msgStatusResId = R.string.dlg_msg_details_msg_status_received; + } else { + msgStatusResId = R.string.dlg_msg_details_msg_status_sent; + } break; - case Message.STATUS_SEND: - case Message.STATUS_SEND_DISPLAYED: - case Message.STATUS_SEND_RECEIVED: default: msgStatusResId = R.string.dlg_msg_details_msg_status_sent; } @@ -154,7 +162,7 @@ public class MessageDetailsDialog extends AbstractAlertDialog { // Get own nick for MUC me = conversation.getMucOptions().getActualNick(); } - if (Message.STATUS_RECEIVED == message.getStatus()) { + if (MessageUtil.isIncomingMessage(message)) { // Sender was chat partner, if the status is for my account received sender.setText(other); // Set receipient to myself in case of normal chat or private message in MUC diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnClickListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnClickListener.java index e3c85fdf..9eaec00c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnClickListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnClickListener.java @@ -1,6 +1,5 @@ package de.thedevstack.conversationsplus.ui.listeners; -import android.app.Activity; import android.content.Intent; import android.view.View; import android.widget.Toast; @@ -11,6 +10,7 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.ui.ConversationActivity; import de.thedevstack.conversationsplus.ui.EditAccountActivity; +import de.thedevstack.conversationsplus.utils.MessageUtil; /** */ @@ -25,7 +25,7 @@ public class ContactPictureOnClickListener implements View.OnClickListener { @Override public void onClick(View view) { - if (message.getStatus() <= Message.STATUS_RECEIVED) { // What does this mean? + if (MessageUtil.isIncomingMessage(message)) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { if (message.getCounterpart() != null) { String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnLongClickListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnLongClickListener.java index 49ff764b..fef10f90 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnLongClickListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ContactPictureOnLongClickListener.java @@ -7,6 +7,7 @@ import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.ui.ConversationActivity; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.ui.QrCodeUtil; /** @@ -22,7 +23,7 @@ public class ContactPictureOnLongClickListener implements View.OnLongClickListen @Override public boolean onLongClick(View view) { - if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { if (message.getConversation().getMode() == Conversation.MODE_MULTI) { if (message.getCounterpart() != null) { String user = message.getCounterpart().getResourcepart(); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java index e6ec7496..a65e9c7a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java @@ -113,19 +113,14 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener { } protected Message createMessage() { - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); - } + final Message message = MessageUtil.createOutgoingMessage(conversation, ""); if (null != conversation.getNextCounterpart() && null == message.getCounterpart()) { message.setCounterpart(conversation.getNextCounterpart()); } //message.setType(Message.TYPE_IMAGE); message.setFileParams(new FileParams()); - message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD); + MessageUtil.setFileStatus(message, FileStatus.NEEDS_UPLOAD); return message; } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java index 709b3f20..616cf962 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java @@ -8,6 +8,7 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.exceptions.FileCopyException; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.ui.UiCallback; @@ -17,18 +18,22 @@ import de.thedevstack.conversationsplus.ui.UiCallback; */ public class ConversationUtil { + public static void markMessagesAsDisplayedUpToMessage(Message message) { + Conversation conversation = message.getConversation(); + + while (null != message && (MessageStatus.RECEIVED == message.getMessageStatus() || MessageStatus.TRANSMITTED == message.getMessageStatus())) { + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.DISPLAYED); + + message = conversation.getMessageBefore(message); + } + } + public static void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - int encryption = conversation.getNextEncryption(); - if (encryption == Message.ENCRYPTION_PGP) { - encryption = Message.ENCRYPTION_DECRYPTED; - } - Message message = new Message(conversation, uri.toString(), encryption); - if (conversation.getNextCounterpart() != null) { - message.setCounterpart(conversation.getNextCounterpart()); - } - if (encryption == Message.ENCRYPTION_DECRYPTED) { + Message message = MessageUtil.createOutgoingMessage(conversation, uri.toString()); + + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { PgpEngine.getInstance().encrypt(message, callback); } else { callback.success(message); @@ -38,18 +43,12 @@ public class ConversationUtil { public static void attachFileToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { - final Message message; - if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) { - message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED); - } else { - message = new Message(conversation, "", conversation.getNextEncryption()); - } - message.setCounterpart(conversation.getNextCounterpart()); - //message.setType(Message.TYPE_FILE); + final Message message = MessageUtil.createOutgoingMessage(conversation, ""); + if (null == message.getFileParams()) { message.setFileParams(new FileParams()); } - message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD); + MessageUtil.setFileStatus(message, FileStatus.NEEDS_UPLOAD); String path = FileUtils.getPath(uri); if (path != null) { message.getFileParams().setPath(path); diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java index 913f2ab5..907e7fd7 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ExceptionHelper.java @@ -95,8 +95,7 @@ public class ExceptionHelper { Jid.fromString(activity.getString(R.string.cplus_bugreport_jabberid)), false); } catch (final InvalidJidException ignored) { } - Message message = new Message(conversation, report - .toString(), Message.ENCRYPTION_NONE); + Message message = MessageUtil.createOutgoingMessage(conversation, report.toString()); service.sendMessage(message); } }); diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java index 662f797a..b4be2d35 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/GeoHelper.java @@ -41,7 +41,7 @@ public class GeoHelper { } final Conversation conversation = message.getConversation(); String label; - if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + if (conversation.getMode() == Conversation.MODE_SINGLE && MessageUtil.isIncomingMessage(message)) { try { label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")"; } catch (UnsupportedEncodingException e) { @@ -55,7 +55,7 @@ public class GeoHelper { locationPluginIntent.putExtra("latitude",latitude); locationPluginIntent.putExtra("longitude",longitude); if (conversation.getMode() == Conversation.MODE_SINGLE) { - if (message.getStatus() == Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { locationPluginIntent.putExtra("name",conversation.getName()); locationPluginIntent.putExtra("jid",message.getCounterpart().toString()); } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageParserUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageParserUtil.java new file mode 100644 index 00000000..5044d132 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageParserUtil.java @@ -0,0 +1,164 @@ +package de.thedevstack.conversationsplus.utils; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageConfirmation; +import de.thedevstack.conversationsplus.xmpp.chatmarkers.ChatMarkers; +import de.thedevstack.conversationsplus.xmpp.receipts.MessageDeliveryReceipts; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +/** + */ +public class MessageParserUtil { + public static MessageConfirmation extractMessageConfirmation(MessagePacket messagePacket) { + if (messagePacket.hasChild(ChatMarkers.MARKABLE) + && messagePacket.hasChild(MessageDeliveryReceipts.REQUEST)) { + return MessageConfirmation.DELIVERY_RECEIPT_CHAT_MARKERS; + } else if (messagePacket.hasChild(ChatMarkers.MARKABLE)) { + return MessageConfirmation.CHAT_MARKERS; + } else if (messagePacket.hasChild(MessageDeliveryReceipts.REQUEST)) { + return MessageConfirmation.DELIVERY_RECEIPT; + } else { + return MessageConfirmation.NONE; + } + } + + public static void extractFileParamsFromBody(Message message) { + if (null == message) { + return; + } + + // Ensure that for every message the fileParams are set after calling this method + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + message.setFileParams(fileParams); + } + + String body = message.getBody(); + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://example.com/image.jpg" text that will not be shown /abc.png" + * or more than one image link in one message. + */ + if (null == body || body.isEmpty() || body.contains(" ")) { + return; + } + + URL url = MessageParserUtil.extractValidUrlFromBody(message); + if (null != url) { + String extension = FileUtils.getRelevantExtension(url); + if (message.isHttpUploaded()) { + fileParams.setUrl(url.toString()); + if (null != extension + && (Transferable.WELL_KNOWN_EXTENSIONS.contains(extension.toLowerCase()) || Transferable.VALID_IMAGE_EXTENSIONS.contains(extension.toLowerCase()))) { + message.setTreatAsDownloadable(Message.Decision.MUST); + } else { + MessageParserUtil.setParamsToAvoidDownload(message, fileParams); + } + + extractFilename(message, url.toString()); + return; + } + + if (extension == null) { + MessageParserUtil.setParamsToAvoidDownload(message, fileParams); + return; + } + byte[] ivAndKey = UrlUtil.getIvAndKeyFromURL(url); + + if (null != ivAndKey) { + if (MimeUtils.guessMimeTypeFromExtension(extension) != null) { + message.setTreatAsDownloadable(Message.Decision.MUST); + fileParams.setKeyAndIv(ivAndKey); + } else { + MessageParserUtil.setParamsToAvoidDownload(message, fileParams); + } + } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension) + || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) { + message.setTreatAsDownloadable(Message.Decision.SHOULD); + MessageUtil.setFileStatus(message, FileStatus.NEEDS_DOWNLOAD); + } else { + MessageParserUtil.setParamsToAvoidDownload(message, fileParams); + } + + if (message.treatAsDownloadable() == Message.Decision.MUST + || message.treatAsDownloadable() == Message.Decision.SHOULD) { + fileParams.setUrl(url.toString()); + extractFilename(message, url.toString()); + MessageUtil.setFileStatus(message, FileStatus.NEEDS_DOWNLOAD); + } + } else { + MessageParserUtil.setParamsToAvoidDownload(message, fileParams); + } + } + + private static void setParamsToAvoidDownload(Message message, FileParams fileParams) { + message.setTreatAsDownloadable(Message.Decision.NEVER); + MessageUtil.setFileStatus(message, FileStatus.UNDEFINED); + } + + private static void extractFilename(Message message, String url) { + String originalFilename = FileUtils.getFilenameFromPath(url); + final String lowerCaseFilename = originalFilename.toLowerCase(); + final String lastPart = FileUtils.getLastExtension(lowerCaseFilename); + + detectAndSetEncryption(lastPart, message); + + String filenameExtension; + if (!lastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + filenameExtension = FileUtils.getSecondToLastExtension(lowerCaseFilename); + originalFilename = originalFilename.replace("." + lastPart, ""); + } else { + filenameExtension = lastPart; + } + message.setRelativeFilePath(message.getUuid() + "." + filenameExtension); + + message.getFileParams().setOriginalFilename(originalFilename); + } + + private static void detectAndSetEncryption(String lastPart, Message message) { + if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { + message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { + message.setEncryption(Message.ENCRYPTION_NONE); + } else if ((message.getEncryption() == Message.ENCRYPTION_OTR + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL) + && message.getFileParams() != null && message.getFileParams().getKey() == null) { + // If an encryption is set for the message, but no key given -> decryption not possible + message.setEncryption(Message.ENCRYPTION_NONE); + } + } + + private static URL extractValidUrlFromBody(Message message) { + if (null == message) { + return null; + } + String body = message.getBody(); + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://example.com/image.jpg" text that will not be shown /abc.png" + * or more than one image link in one message. + */ + if (null == body || body.isEmpty() || body.contains(" ")) { + return null; + } + + try { + URL url = new URL(body); + if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { + return null; + } + + return url; + } catch (MalformedURLException e) { + return null; + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java index 2e346486..0be1461b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -3,138 +3,35 @@ package de.thedevstack.conversationsplus.utils; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import java.net.MalformedURLException; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; +import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.enums.MessageConfirmation; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.persistance.DatabaseBackend; import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.utils.messaging.MessageReceiptUtil; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.tzur.conversations.Settings; /** * Utility class to work with messages. */ public final class MessageUtil { - public static void extractFileParamsFromBody(Message message) { - if (null == message) { - return; - } - - // Ensure that for every message the fileParams are set after calling this method - FileParams fileParams = message.getFileParams(); - if (null == fileParams) { - fileParams = new FileParams(); - message.setFileParams(fileParams); - } - - String body = message.getBody(); - /** - * there are a few cases where spaces result in an unwanted behavior, e.g. - * "http://example.com/image.jpg" text that will not be shown /abc.png" - * or more than one image link in one message. - */ - if (null == body || body.isEmpty() || body.contains(" ")) { - return; - } - - try { - URL url = new URL(body); - if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - return; - } - String extension = FileUtils.getRelevantExtension(url); - if (message.isHttpUploaded()) { - fileParams.setUrl(url.toString()); - if (null != extension - && (Transferable.WELL_KNOWN_EXTENSIONS.contains(extension.toLowerCase()) || Transferable.VALID_IMAGE_EXTENSIONS.contains(extension.toLowerCase()))) { - message.setTreatAsDownloadable(Message.Decision.MUST); - } else { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - } - - extractFilename(message, url.toString()); - return; - } - - if (extension == null) { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - return; - } - byte[] ivAndKey = UrlUtil.getIvAndKeyFromURL(url); - - if (null != ivAndKey) { - if (MimeUtils.guessMimeTypeFromExtension(extension) != null) { - message.setTreatAsDownloadable(Message.Decision.MUST); - fileParams.setKeyAndIv(ivAndKey); - } else { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - } - } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension) - || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) { - message.setTreatAsDownloadable(Message.Decision.SHOULD); - } else { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - } - - if (message.treatAsDownloadable() == Message.Decision.MUST - || message.treatAsDownloadable() == Message.Decision.SHOULD) { - fileParams.setUrl(url.toString()); - extractFilename(message, url.toString()); - } - } catch (MalformedURLException e) { - message.setTreatAsDownloadable(Message.Decision.NEVER); - fileParams.setFileStatus(FileStatus.UNDEFINED); - } - } - - private static void extractFilename(Message message, String url) { - String originalFilename = FileUtils.getFilenameFromPath(url); - final String lowerCaseFilename = originalFilename.toLowerCase(); - final String lastPart = FileUtils.getLastExtension(lowerCaseFilename); - - detectAndSetEncryption(lastPart, message); - - String filenameExtension; - if (!lastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { - filenameExtension = FileUtils.getSecondToLastExtension(lowerCaseFilename); - originalFilename = originalFilename.replace("." + lastPart, ""); - } else { - filenameExtension = lastPart; - } - message.setRelativeFilePath(message.getUuid() + "." + filenameExtension); - - message.getFileParams().setOriginalFilename(originalFilename); - } - - private static void detectAndSetEncryption(String lastPart, Message message) { - if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { - message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR - && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { - message.setEncryption(Message.ENCRYPTION_NONE); - } else if ((message.getEncryption() == Message.ENCRYPTION_OTR - || message.getEncryption() == Message.ENCRYPTION_AXOLOTL) - && message.getFileParams() != null && message.getFileParams().getKey() == null) { - // If an encryption is set for the message, but no key given -> decryption not possible - message.setEncryption(Message.ENCRYPTION_NONE); - } - } - /** * Checks if an attached file is an image or not. * Prerequisite for calling this method: The check if a file is attached is done before. @@ -179,8 +76,8 @@ public final class MessageUtil { public static boolean needsDownload(Message message) { FileStatus fileStatus = (null != message.getFileParams()) ? message.getFileParams().getFileStatus() : null; return (message.hasFileAttached() || MessageUtil.hasDownloadableLink(message)) - &&(null == fileStatus - || (null != fileStatus && (fileStatus == FileStatus.NEEDS_DOWNLOAD || fileStatus == FileStatus.UNDEFINED))) + && (null == fileStatus + || (fileStatus == FileStatus.NEEDS_DOWNLOAD || fileStatus == FileStatus.UNDEFINED)) && message.treatAsDownloadable() != Message.Decision.NEVER; } @@ -190,31 +87,75 @@ public final class MessageUtil { && null != url; } + public static boolean isOutgoingMessage(Message message) { + return MessageDirection.OUT == message.getDirection(); + } + + public static boolean isIncomingMessage(Message message) { + return MessageDirection.IN == message.getDirection(); + } + public static boolean isMessageSent(Message message) { - switch (message.getStatus()) { - case Message.STATUS_SEND: - case Message.STATUS_SEND_DISPLAYED: - case Message.STATUS_SEND_FAILED: - case Message.STATUS_SEND_RECEIVED: - return true; - default: - return false; + return MessageUtil.isOutgoingMessage(message) + && MessageStatus.TRANSMITTED == message.getMessageStatus(); + } + + public static boolean isMessageReceived(Message message) { + return MessageUtil.isIncomingMessage(message) + && MessageStatus.TRANSMITTED == message.getMessageStatus(); + } + + public static boolean isMessageTransmittedOrDisplayedOrReceived(Message message) { + MessageStatus status = message.getMessageStatus(); + return MessageStatus.TRANSMITTED == status + || MessageStatus.RECEIVED == status + || MessageStatus.DISPLAYED == status; + } + + public static void setAndSaveStatusForFileDeleted(Message message) { + message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); + //FIXME: File Status needs to be changed + MessageStatus msgStatus = message.getMessageStatus(); + if (MessageUtil.isOutgoingMessage(message) + && (MessageStatus.TRANSMITTING == msgStatus || MessageStatus.WAITING == msgStatus)) { + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.FAILED); // FIXME: add some information why this is failed } } public static void setAndSaveFileStatus(Message message, FileStatus fileStatus) { + if (setFileStatus(message, fileStatus)) { + DatabaseBackend.getInstance().updateMessage(message); + UiUpdateHelper.updateConversationUi(); + } + } + + public static boolean setFileStatus(Message message, FileStatus fileStatus) { message.getFileParams().setFileStatus(fileStatus); - DatabaseBackend.getInstance().updateMessage(message); - UiUpdateHelper.updateConversationUi(); + if (FileStatus.DOWNLOAD_FAILED == fileStatus + || FileStatus.UPLOAD_FAILED == fileStatus) { + setMessageStatus(message, MessageStatus.FAILED); + } else if (FileStatus.DOWNLOADED == fileStatus) { + setMessageStatus(message, MessageStatus.TRANSMITTED); + } + return true; + } + + public static boolean markMessageAsReceived(Account account, Jid from, String id) { + Message message = XmppConnectionServiceAccessor.xmppConnectionService.getMessage(account, from, id); + if (null != message) { + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.RECEIVED); + return true; + } + return false; } - public static boolean markMessage(Conversation conversation, String uuid, int status) { + public static boolean setAndSaveMessageStatus(Conversation conversation, String uuid, MessageStatus newStatus) { if (uuid == null) { return false; } else { Message message = conversation.findSentMessageWithUuid(uuid); if (message != null) { - markMessage(message, status); + setAndSaveMessageStatus(message, newStatus); return true; } else { return false; @@ -222,15 +163,38 @@ public final class MessageUtil { } } - public static void markMessage(Message message, int status) { - if (status == Message.STATUS_SEND_FAILED - && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message - .getStatus() == Message.STATUS_SEND_DISPLAYED)) { - return; + public static void setAndSaveMessageStatus(Message message, MessageStatus newStatus) { + if (setMessageStatus(message, newStatus)) { + DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateMessage(message); + UiUpdateHelper.updateConversationUi(); + } + } + + private static boolean sendReceipt(Message message) { + return Settings.CONFIRM_MESSAGE_RECEIVED // Only if user allows to send message confirmations + && isIncomingMessage(message) // Only if a message is incoming + && MessageConfirmation.NONE != message.getConfirmation() // Only if message contained an confirmation request + && message.getRemoteMsgId() != null // Only if there is an remote id + && !message.isMamReceived() // Only if it is not received using MAM + && !message.isCarbon() // Only if it is not received with carbons + && !message.isRead(); // Only if the message is not read yet + } + + public static boolean setMessageStatus(Message message, MessageStatus newStatus) { + MessageStatus currentStatus = message.getMessageStatus(); + if ((MessageStatus.FAILED == newStatus && (MessageStatus.RECEIVED == currentStatus || MessageStatus.DISPLAYED == currentStatus))) { + return false; + } + message.setMessageStatus(newStatus); + if (MessageStatus.TRANSMITTED == newStatus) { + if (sendReceipt(message)) { + Logging.d("message-util", "Send message receipt from setMessageStatus"); + MessageReceiptUtil.sendMessageReceipts(message); + } + Logging.d("message-util", "Push notification for message"); + XmppConnectionServiceAccessor.xmppConnectionService.getNotificationService().push(message); } - message.setStatus(status); - DatabaseBackend.getInstance(ConversationsPlusApplication.getAppContext()).updateMessage(message); - UiUpdateHelper.updateConversationUi(); + return true; } public static boolean wasHighlightedOrPrivate(final Message message) { @@ -302,6 +266,29 @@ public final class MessageUtil { } } + public static Message createStatusMessage(Conversation conversation, String body) { + final Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + message.setBody(body); + return message; + } + + public static Message createOutgoingMessage(Conversation conversation, String body) { + int encryption = conversation.getNextEncryption(); + if (encryption == Message.ENCRYPTION_PGP) { + encryption = Message.ENCRYPTION_DECRYPTED; + } + Message message = new Message(conversation, body, encryption); + message.setMessageStatus(MessageStatus.WAITING); + message.setDirection(MessageDirection.OUT); + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + } + + return message; + } + private MessageUtil() { // Static helper class } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java index d27481df..31089a62 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java @@ -20,6 +20,7 @@ import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Presence; import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.xmpp.jid.Jid; public class UIHelper { @@ -89,8 +90,8 @@ public class UIHelper { return sameDay(date,new Date(System.currentTimeMillis())); } - public static boolean sameDay(long timestamp1, long timestamp2) { - return sameDay(new Date(timestamp1),new Date(timestamp2)); + public static boolean sameDay(long timestamp, long timestamp2) { + return sameDay(new Date(timestamp),new Date(timestamp2)); } private static boolean sameDay(Date a, Date b) { @@ -147,8 +148,7 @@ public class UIHelper { getFileDescriptionString(context,message)),true); case Transferable.STATUS_DOWNLOADING: return new Pair<>(context.getString(R.string.receiving_x_file, - getFileDescriptionString(context,message), - d.getProgress()),true); + getFileDescriptionString(context,message)),true); case Transferable.STATUS_OFFER: case Transferable.STATUS_OFFER_CHECK_FILESIZE: return new Pair<>(context.getString(R.string.x_file_offered_for_download, @@ -158,7 +158,7 @@ public class UIHelper { case Transferable.STATUS_FAILED: return new Pair<>(context.getString(R.string.file_transmission_failed),true); case Transferable.STATUS_UPLOADING: - if (message.getStatus() == Message.STATUS_OFFERED) { + if (message.getFileParams().getFileStatus() == FileStatus.NEEDS_UPLOAD) { return new Pair<>(context.getString(R.string.offering_x_file, getFileDescriptionString(context, message)), true); } else { @@ -173,7 +173,7 @@ public class UIHelper { } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { return new Pair<>(context.getString(R.string.decryption_failed), true); } else if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) { - if (message.getStatus() == Message.STATUS_RECEIVED) { + if (MessageUtil.isMessageReceived(message)) {// Todo: Check if Transmitted && Displayed && Received should be used here return new Pair<>(context.getString(R.string.received_x_file, getFileDescriptionString(context, message)), true); } else { @@ -183,7 +183,7 @@ public class UIHelper { if (message.hasMeCommand()) { return new Pair<>(message.getBodyReplacedMeCommand(UIHelper.getMessageDisplayName(message)), false); } else if (GeoHelper.isGeoUri(message.getBody())) { - if (message.getStatus() == Message.STATUS_RECEIVED) { + if (MessageUtil.isMessageReceived(message)) { // Todo: Check if Transmitted && Displayed && Received should be used here return new Pair<>(context.getString(R.string.received_location), true); } else { return new Pair<>(context.getString(R.string.location), true); @@ -233,7 +233,7 @@ public class UIHelper { public static String getMessageDisplayName(final Message message) { final Conversation conversation = message.getConversation(); - if (message.getStatus() == Message.STATUS_RECEIVED) { + if (MessageUtil.isIncomingMessage(message)) { final Contact contact = message.getContact(); if (conversation.getMode() == Conversation.MODE_MULTI) { if (contact != null) { @@ -304,7 +304,7 @@ public class UIHelper { public static boolean receivedLocationQuestion(Message message) { if (message == null - || message.getStatus() != Message.STATUS_RECEIVED + || MessageUtil.isOutgoingMessage(message) || message.getType() != Message.TYPE_TEXT) { return false; } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/messaging/MessageReceiptUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/messaging/MessageReceiptUtil.java new file mode 100644 index 00000000..a3ac1c3a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/utils/messaging/MessageReceiptUtil.java @@ -0,0 +1,41 @@ +package de.thedevstack.conversationsplus.utils.messaging; + +import java.util.ArrayList; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.MessageConfirmation; +import de.thedevstack.conversationsplus.utils.XmppSendUtil; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.chatmarkers.ChatMarkers; +import de.thedevstack.conversationsplus.xmpp.receipts.MessageDeliveryReceipts; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +/** + */ +public final class MessageReceiptUtil { + public static void sendMessageReceipts(Message message) { + MessageConfirmation confirmation = message.getConfirmation(); + ArrayList<Element> receipts = new ArrayList<>(); + if (MessageConfirmation.DELIVERY_RECEIPT_CHAT_MARKERS == confirmation) { + receipts.add(ChatMarkers.RECEIVED.getXmlElement()); + receipts.add(MessageDeliveryReceipts.RECEIVED.getXmlElement()); + } else if (MessageConfirmation.CHAT_MARKERS == confirmation) { + receipts.add(ChatMarkers.RECEIVED.getXmlElement()); + } else { + receipts.add(MessageDeliveryReceipts.RECEIVED.getXmlElement()); + } + + Account account = message.getConversation().getAccount(); + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType((message.getConversation().getMode() == Conversation.MODE_SINGLE ? MessagePacket.TYPE_CHAT : MessagePacket.TYPE_GROUPCHAT)); + receivedPacket.setTo(message.getCounterpart()); + receivedPacket.setFrom(account.getJid()); + for (Element receipt : receipts) { + receivedPacket.addChild(receipt).setAttribute("id", message.getRemoteMsgId()); + } + + XmppSendUtil.sendMessagePacket(account, receivedPacket); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xml/Element.java b/src/main/java/de/thedevstack/conversationsplus/xml/Element.java index 23836365..0b8c2035 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xml/Element.java +++ b/src/main/java/de/thedevstack/conversationsplus/xml/Element.java @@ -74,6 +74,22 @@ public class Element { return null; } + public Element findChildRecursive(String name, String xmlns) { + Element foundChild = null; + for (Element child : this.children) { + if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { + foundChild = child; + break; + } else { + foundChild = findChildRecursive(name, xmlns); + if (null != foundChild) { + break; + } + } + } + return foundChild; + } + public Element findChild(XmlElementContainer container) { Element element = container.getXmlElement(); return findChild(element.getName(), element.getNamespace()); @@ -92,9 +108,18 @@ public class Element { return findChild(name, xmlns) != null; } - public boolean hasChild(XmlElementContainer container) { + public boolean hasChildRecursive(String name, String xmlns) { + return null != findChildRecursive(name, xmlns); + } + + public boolean hasChildRecursive(XmlElementContainer container) { + Element element = container.getXmlElement(); + return hasChildRecursive(element.getName(), element.getNamespace()); + } + + public boolean hasChild(XmlElementContainer container) { Element element = container.getXmlElement(); - return hasChild(element.getName(), element.getNamespace()); + return hasChild(element.getName(), element.getNamespace()); } public List<Element> getChildren() { diff --git a/src/main/java/de/thedevstack/conversationsplus/xml/XmlHelper.java b/src/main/java/de/thedevstack/conversationsplus/xml/XmlHelper.java index 1287f73f..36a536aa 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xml/XmlHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/xml/XmlHelper.java @@ -1,4 +1,4 @@ -package de.thedevstack.conversationsplus.utils; +package de.thedevstack.conversationsplus.xml; public class XmlHelper { public static String encodeEntities(String content) { diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java index 6274aaa2..de5e2b20 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/PacketReceived.java @@ -1,5 +1,5 @@ package de.thedevstack.conversationsplus.xmpp; -public abstract interface PacketReceived { +public interface PacketReceived { } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java index 2b366dbb..f6e6edb8 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/XmppConnection.java @@ -62,9 +62,11 @@ import de.thedevstack.conversationsplus.crypto.sasl.ScramSha1; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.ServiceDiscoveryResult; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.generator.IqGenerator; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.DNSHelper; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.SSLSocketHelper; import de.thedevstack.conversationsplus.utils.Xmlns; import de.thedevstack.conversationsplus.xml.Element; @@ -492,10 +494,10 @@ public class XmppConnection implements Runnable { for(AbstractAcknowledgeableStanza packet : failedStanzas) { if (packet instanceof MessagePacket) { MessagePacket message = (MessagePacket) packet; - mXmppConnectionService.markMessage(account, + Message msg = mXmppConnectionService.getMessage(account, message.getTo().toBareJid(), - message.getId(), - Message.STATUS_UNSEND); + message.getId()); + MessageUtil.setAndSaveMessageStatus(msg, MessageStatus.TRANSMITTING); } sendPacket(packet); } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkers.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkers.java new file mode 100644 index 00000000..a1822b7d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkers.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.xmpp.chatmarkers; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xml.XmlElementContainer; + +/** + * Representation of the ChatMarkers hint. + * <pre> + * <markable xmlns="urn:xmpp:chat-markers:0"/> + * </pre> + */ +public enum ChatMarkers implements XmlElementContainer { + MARKABLE("markable"), + RECEIVED("received"), + DISPLAYED("displayed"), + ACKNOWLEDGED("acknowledged"); + + private final Element xmlElement; + + ChatMarkers(String elementName) { + this.xmlElement = new Element(elementName, ChatMarkersXep.NAMESPACE); + } + + @Override + public Element getXmlElement() { + return xmlElement; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersHandler.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersHandler.java new file mode 100644 index 00000000..a8277a04 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersHandler.java @@ -0,0 +1,11 @@ +package de.thedevstack.conversationsplus.xmpp.chatmarkers; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + */ +public interface ChatMarkersHandler { + void messageReceived(Account account, Jid from, String messageId); + void messageDisplayed(Account account, Jid from, String messageId); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersPacketHandler.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersPacketHandler.java new file mode 100644 index 00000000..89fd945a --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersPacketHandler.java @@ -0,0 +1,24 @@ +package de.thedevstack.conversationsplus.xmpp.chatmarkers; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.stanzas.MessagePacket; + +/** + */ +public class ChatMarkersPacketHandler { + private ChatMarkersHandler handler; + + public void handleMessagePacket(Account account, MessagePacket packet, Message message) { + message.markable = packet.hasChild("markable", ChatMarkersXep.NAMESPACE); + Element received = packet.findChild("received", ChatMarkersXep.NAMESPACE); + if (null != received && null != handler) { + handler.messageReceived(account, packet.getFrom().toBareJid(), received.getAttribute("id")); + } + Element displayed = packet.findChild("displayed", ChatMarkersXep.NAMESPACE); + if (null != displayed && null != handler) { + handler.messageDisplayed(account, packet.getFrom().toBareJid(), displayed.getAttribute("id")); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersXep.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersXep.java new file mode 100644 index 00000000..58f3f66e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/chatmarkers/ChatMarkersXep.java @@ -0,0 +1,49 @@ +package de.thedevstack.conversationsplus.xmpp.chatmarkers; + +import de.thedevstack.conversationsplus.xmpp.AbstractXep; +import de.thedevstack.conversationsplus.xmpp.IqPacketHandler; + +/** + */ +public class ChatMarkersXep extends AbstractXep { + public static final String NAMESPACE = "urn:xmpp:chat-markers:0"; + + public ChatMarkersXep(boolean enabled) { + super(enabled); + } + + @Override + public String xepNumber() { + return "0333"; + } + + @Override + public String shortName() { + return ""; + } + + @Override + public String name() { + return "Chat Markers"; + } + + @Override + public String namespace() { + return NAMESPACE; + } + + @Override + public String featureNamespace() { + return namespace(); + } + + @Override + public String elementName() { + return null; + } + + @Override + public IqPacketHandler handler() { + return null; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/disco/FeatureRegistry.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/disco/FeatureRegistry.java index 1c266545..cb53e993 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/disco/FeatureRegistry.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/disco/FeatureRegistry.java @@ -34,7 +34,6 @@ public final class FeatureRegistry { ChatState.NAMESPACE, AxolotlService.PEP_DEVICE_LIST+"+notify"}; private static final String[] LEGACY_MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; private final List<String> features = new ArrayList<>(); diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java index 13fb5ad1..1330b18d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java @@ -1,6 +1,7 @@ package de.thedevstack.conversationsplus.xmpp.httpuploadim; import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xml.XmlElementContainer; /** * Representation of the HttpUploadHint. @@ -8,18 +9,23 @@ import de.thedevstack.conversationsplus.xml.Element; * <httpupload xmlns="urn:xmpp:hints"/> * </pre> */ -public class HttpUploadHint extends Element { +public enum HttpUploadHint implements XmlElementContainer { + HTTP_UPLOAD_HINT("httpupload"); + + private Element xmlElement; + + HttpUploadHint(String elementName) { + this.xmlElement = new Element(elementName, NAMESPACE); + } + + @Override + public Element getXmlElement() { + return xmlElement; + } + /** * The namespace of message processing hints as defined in XEP-0334. * @see <a href="http://xmpp.org/extensions/xep-0334.html">http://xmpp.org/extensions/xep-0334.html</a> */ public static final String NAMESPACE = "urn:xmpp:hints"; - /** - * The element name of the hint for an http upload. - */ - public static final String ELEMENT_NAME = "httpupload"; - - public HttpUploadHint() { - super(ELEMENT_NAME, NAMESPACE); - } } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java index 49a10684..29e83c34 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -17,6 +17,8 @@ import java.util.concurrent.ConcurrentHashMap; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.enums.MessageDirection; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.Config; @@ -105,7 +107,7 @@ public class JingleConnection implements Transferable { sendSuccess(); MessageUtil.updateFileParams(message); mXmppConnectionService.databaseBackend.createMessage(message); - MessageUtil.markMessage(message,Message.STATUS_RECEIVED); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTED); if (acceptedAutomatically) { message.markUnread(); JingleConnection.this.mXmppConnectionService.getNotificationService().push(message); @@ -297,7 +299,8 @@ public class JingleConnection implements Transferable { .findOrCreateConversation(account, packet.getFrom().toBareJid(), false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); - this.message.setStatus(Message.STATUS_RECEIVED); + MessageUtil.setMessageStatus(this.message, MessageStatus.TRANSMITTING); + this.message.setDirection(MessageDirection.IN); this.mStatus = Transferable.STATUS_OFFER; this.message.setTransferable(this); final Jid from = packet.getFrom(); @@ -458,7 +461,7 @@ public class JingleConnection implements Transferable { if (packet.getType() == IqPacket.TYPE.RESULT) { Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": other party received offer"); mJingleStatus = JINGLE_STATUS_INITIATED; - MessageUtil.markMessage(message, Message.STATUS_OFFERED); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTING); } else { fail(); } @@ -551,7 +554,7 @@ public class JingleConnection implements Transferable { mergeCandidates(JingleCandidate.parse(content.socks5transport() .getChildren())); this.mJingleStatus = JINGLE_STATUS_ACCEPTED; - MessageUtil.markMessage(message, Message.STATUS_UNSEND); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.TRANSMITTING); this.connectNextCandidate(); return true; } @@ -711,7 +714,7 @@ public class JingleConnection implements Transferable { this.sendJinglePacket(packet); this.disconnectSocks5Connections(); this.mJingleStatus = JINGLE_STATUS_FINISHED; - this.message.setStatus(Message.STATUS_RECEIVED); + MessageUtil.setMessageStatus(message, MessageStatus.TRANSMITTED); this.message.setTransferable(null); this.mXmppConnectionService.updateMessage(message); this.mJingleConnectionManager.finishConnection(this); @@ -783,7 +786,7 @@ public class JingleConnection implements Transferable { private void receiveSuccess() { this.mJingleStatus = JINGLE_STATUS_FINISHED; - MessageUtil.markMessage(this.message,Message.STATUS_SEND_RECEIVED); + MessageUtil.setAndSaveMessageStatus(this.message, MessageStatus.RECEIVED); this.disconnectSocks5Connections(); if (this.transport != null && this.transport instanceof JingleInbandTransport) { this.transport.disconnect(); @@ -806,7 +809,7 @@ public class JingleConnection implements Transferable { } this.mXmppConnectionService.updateConversationUi(); } else { - MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED); + MessageUtil.setAndSaveMessageStatus(this.message, MessageStatus.CANCELED); this.message.setTransferable(null); } } @@ -827,7 +830,7 @@ public class JingleConnection implements Transferable { } this.mXmppConnectionService.updateConversationUi(); } else { - MessageUtil.markMessage(this.message, Message.STATUS_SEND_FAILED); + MessageUtil.setAndSaveMessageStatus(this.message, MessageStatus.FAILED); this.message.setTransferable(null); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java index 30ed9024..46fa02b0 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnectionManager.java @@ -13,6 +13,7 @@ import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; +import de.thedevstack.conversationsplus.enums.MessageStatus; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.MessageUtil; @@ -62,7 +63,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { old.cancel(); } JingleConnection connection = new JingleConnection(this); - MessageUtil.markMessage(message,Message.STATUS_WAITING); + MessageUtil.setAndSaveMessageStatus(message, MessageStatus.WAITING); connection.init(message); this.connections.add(connection); return connection; diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/muc/MucPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/muc/MucPacketParser.java new file mode 100644 index 00000000..d7ec6b38 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/muc/MucPacketParser.java @@ -0,0 +1,62 @@ +package de.thedevstack.conversationsplus.xmpp.muc; + +import de.thedevstack.conversationsplus.entities.Account; +import de.thedevstack.conversationsplus.entities.Conversation; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +/** + */ +public class MucPacketParser { + private static class Invite { + Jid jid; + String password; + Invite(Jid jid, String password) { + this.jid = jid; + this.password = password; + } + + public boolean execute(Account account) { + if (jid != null) { + Conversation conversation = XmppConnectionServiceAccessor.xmppConnectionService.findOrCreateConversation(account, jid, true); + if (!conversation.getMucOptions().online()) { + conversation.getMucOptions().setPassword(password); + DatabaseBackend.getInstance().updateConversation(conversation); + XmppConnectionServiceAccessor.xmppConnectionService.joinMuc(conversation); + UiUpdateHelper.updateConversationUi(); + } + return true; + } + return false; + } + } + + private static Invite extractInvite(Element message) { + Element x = message.findChild("x", "http://jabber.org/protocol/muc#user"); + if (x != null) { + Element invite = x.findChild("invite"); + if (invite != null) { + Element pw = x.findChild("password"); + return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null); + } + } else { + x = message.findChild("x","jabber:x:conference"); + if (x != null) { + return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password")); + } + } + return null; + } + + public static boolean extractAndExecuteInvite(Account account, Element packet) { + Invite invite = extractInvite(packet); + if (invite != null && invite.execute(account)) { + return true; + } + + return false; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/receipts/MessageDeliveryReceipts.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/receipts/MessageDeliveryReceipts.java new file mode 100644 index 00000000..1bc86e37 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/receipts/MessageDeliveryReceipts.java @@ -0,0 +1,24 @@ +package de.thedevstack.conversationsplus.xmpp.receipts; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xml.XmlElementContainer; + +/** + */ +public enum MessageDeliveryReceipts implements XmlElementContainer { + REQUEST("request"), + RECEIVED("received"); + + public static final String NAMESPACE = "urn:xmpp:receipts"; + + private final Element xmlElement; + + MessageDeliveryReceipts(String elementName) { + this.xmlElement = new Element(elementName, NAMESPACE); + } + + @Override + public Element getXmlElement() { + return xmlElement; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java index e14cdacd..5e9d5a9d 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/stanzas/MessagePacket.java @@ -84,7 +84,7 @@ public class MessagePacket extends AbstractAcknowledgeableStanza { return null; } Long timestamp = AbstractParser.getTimestamp(forwarded,null); - return new Pair(packet,timestamp); + return new Pair<>(packet,timestamp); } public static MessagePacket create(Element element) { |