diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 559e2f2d..70d852fe 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -102,9 +102,7 @@ public class Bookmark extends Element implements ListItem { } public boolean autojoin() { - String autojoin = this.getAttribute("autojoin"); - return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin - .equalsIgnoreCase("1"))); + return this.getAttributeAsBoolean("autojoin"); } public String getPassword() { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5d674748..56a0776f 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -4,8 +4,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -94,4 +96,20 @@ public class IqGenerator extends AbstractGenerator { } return packet; } + + public IqPacket queryMessageArchiveManagement(MessageArchiveService.Query mam) { + final IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + Element query = packet.query("urn:xmpp:mam:0"); + query.setAttribute("queryid",mam.getQueryId()); + Data data = new Data(); + data.setFormType("urn:xmpp:mam:0"); + data.put("with",mam.getWith().toString()); + data.put("start",getTimestamp(mam.getStart())); + data.put("end",getTimestamp(mam.getEnd())); + query.addChild(data); + if (mam.getAfter() != null) { + query.addChild("set", "http://jabber.org/protocol/rsm").addChild("after").setContent(mam.getAfter()); + } + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index fd9e1b6c..b902db51 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -272,6 +272,58 @@ public class MessageParser extends AbstractParser implements return finishedMessage; } + private Message parseMamMessage(MessagePacket packet, final Account account) { + final Element result = packet.findChild("result","urn:xmpp:mam:0"); + if (result == null ) { + return null; + } + final Element forwarded = result.findChild("forwarded","urn:xmpp:forward:0"); + if (forwarded == null) { + return null; + } + final Element message = forwarded.findChild("message"); + if (message == null) { + return null; + } + final Element body = message.findChild("body"); + if (body == null || message.hasChild("private","urn:xmpp:carbons:2") || message.hasChild("no-copy","urn:xmpp:hints")) { + return null; + } + int encryption; + String content = getPgpBody(message); + if (content != null) { + encryption = Message.ENCRYPTION_PGP; + } else { + encryption = Message.ENCRYPTION_NONE; + content = body.getContent(); + } + if (content == null) { + return null; + } + final long timestamp = getTimestamp(forwarded); + final Jid to = message.getAttributeAsJid("to"); + final Jid from = message.getAttributeAsJid("from"); + Jid counterpart; + int status; + Conversation conversation; + if (from!=null && to != null && from.toBareJid().equals(account.getJid().toBareJid())) { + status = Message.STATUS_SEND; + conversation = this.mXmppConnectionService.findOrCreateConversation(account,to.toBareJid(),false); + counterpart = to; + } else if (from !=null && to != null) { + status = Message.STATUS_RECEIVED; + conversation = this.mXmppConnectionService.findOrCreateConversation(account,from.toBareJid(),false); + counterpart = from; + } else { + return null; + } + Message finishedMessage = new Message(conversation,content,encryption,status); + finishedMessage.setTime(timestamp); + finishedMessage.setCounterpart(counterpart); + Log.d(Config.LOGTAG,"received mam message "+content); + return finishedMessage; + } + private void parseError(final MessagePacket packet, final Account account) { final Jid from = packet.getFrom(); mXmppConnectionService.markMessage(account, from.toBareJid(), @@ -445,6 +497,17 @@ public class MessageParser extends AbstractParser implements message.markUnread(); } } + } else if (packet.hasChild("result","urn:xmpp:mam:0")) { + message = parseMamMessage(packet, account); + if (message != null) { + Conversation conversation = message.getConversation(); + conversation.add(message); + mXmppConnectionService.databaseBackend.createMessage(message); + } + return; + } else if (packet.hasChild("fin","urn:xmpp:mam:0")) { + Element fin = packet.findChild("fin","urn:xmpp:mam:0"); + mXmppConnectionService.getMessageArchiveService().processFin(fin); } else { parseNonMessage(packet, account); } @@ -493,7 +556,6 @@ public class MessageParser extends AbstractParser implements && conversation.getOtrSession() != null && !conversation.getOtrSession().getSessionID().getUserID() .equals(message.getCounterpart().getResourcepart())) { - Log.d(Config.LOGTAG, "ending because of reasons"); conversation.endOtrIfNeeded(); } @@ -506,7 +568,7 @@ public class MessageParser extends AbstractParser implements if (message.trusted() && message.bodyContainsDownloadable()) { this.mXmppConnectionService.getHttpConnectionManager() .createNewConnection(message); - } else { + } else if (!message.isRead()) { mXmppConnectionService.getNotificationService().push(message); } mXmppConnectionService.updateConversationUi(); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java new file mode 100644 index 00000000..4f47cdbe --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -0,0 +1,137 @@ +package eu.siacs.conversations.services; + +import android.util.Log; + +import java.math.BigInteger; +import java.util.HashSet; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class MessageArchiveService { + + private final XmppConnectionService mXmppConnectionService; + + private final HashSet queries = new HashSet(); + + public MessageArchiveService(final XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void query(final Conversation conversation) { + synchronized (this.queries) { + final Account account = conversation.getAccount(); + long start = conversation.getLastMessageReceived(); + long end = account.getXmppConnection().getLastSessionEstablished(); + final Query query = new Query(conversation, start, end); + this.queries.add(query); + IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, packet.toString()); + } + }); + } + } + + public void processFin(Element fin) { + if (fin == null) { + return; + } + Query query = findQuery(fin.getAttribute("queryid")); + if (query == null) { + return; + } + Log.d(Config.LOGTAG,"fin "+fin.toString()); + boolean complete = fin.getAttributeAsBoolean("complete"); + Element set = fin.findChild("set","http://jabber.org/protocol/rsm"); + Element last = set == null ? null : set.findChild("last"); + if (complete || last == null) { + Log.d(Config.LOGTAG,"completed mam query for "+query.getWith().toString()); + synchronized (this.queries) { + this.queries.remove(query); + } + } else { + Query nextQuery = query.next(last == null ? null : last.getContent()); + IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(nextQuery); + synchronized (this.queries) { + this.queries.remove(query); + this.queries.add(nextQuery); + } + Log.d(Config.LOGTAG,packet.toString()); + this.mXmppConnectionService.sendIqPacket(query.getConversation().getAccount(),packet,new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG,packet.toString()); + } + }); + } + } + + private Query findQuery(String id) { + if (id == null) { + return null; + } + synchronized (this.queries) { + for(Query query : this.queries) { + if (query.getQueryId().equals(id)) { + return query; + } + } + return null; + } + } + + public class Query { + private long start; + private long end; + private Jid with; + private String queryId; + private String after = null; + private Conversation conversation; + + public Query(Conversation conversation, long start, long end) { + this.conversation = conversation; + this.with = conversation.getContactJid().toBareJid(); + this.start = start; + this.end = end; + this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + } + + public Query next(String after) { + Query query = new Query(this.conversation,this.start,this.end); + query.after = after; + return query; + } + + public String getAfter() { + return after; + } + + public String getQueryId() { + return queryId; + } + + public Jid getWith() { + return with; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + + public Conversation getConversation() { + return conversation; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b9da4a81..9d73868c 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -73,6 +73,7 @@ import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesAvailable; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -141,6 +142,7 @@ public class XmppConnectionService extends Service { private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); private AvatarService mAvatarService = new AvatarService(this); + private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private OnConversationUpdate mOnConversationUpdate = null; private Integer convChangedListenerCount = 0; private OnAccountUpdate mOnAccountUpdate = null; @@ -203,6 +205,12 @@ public class XmppConnectionService extends Service { getNotificationService().updateErrorNotification(); } }; + private OnAdvancedStreamFeaturesAvailable onAdvancedStreamFeaturesAvailable = new OnAdvancedStreamFeaturesAvailable() { + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + queryMessagesFromArchive(account); + } + }; private int accountChangedListenerCount = 0; private OnRosterUpdate mOnRosterUpdate = null; private int rosterChangedListenerCount = 0; @@ -583,8 +591,8 @@ public class XmppConnectionService extends Service { connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); connection.setOnJinglePacketReceivedListener(this.jingleListener); connection.setOnBindListener(this.mOnBindListener); - connection - .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + connection.setOnAdvancedStreamFeaturesAvailableListener(this.onAdvancedStreamFeaturesAvailable); return connection; } @@ -1231,6 +1239,19 @@ public class XmppConnectionService extends Service { } } + private void queryMessagesFromArchive(final Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { + List conversations = getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account) { + this.mMessageArchiveService.query(conversation); + } + } + } else { + Log.d(Config.LOGTAG,"no mam available"); + } + } + public void joinMuc(Conversation conversation) { Account account = conversation.getAccount(); account.pendingConferenceJoins.remove(conversation); @@ -1255,7 +1276,6 @@ public class XmppConnectionService extends Service { packet.addChild("status").setContent("online"); packet.addChild("x", "jabber:x:signed").setContent(sig); } - Log.d(Config.LOGTAG,packet.toString()); sendPresencePacket(account, packet); if (!joinJid.equals(conversation.getContactJid())) { conversation.setContactJid(joinJid); @@ -2033,6 +2053,10 @@ public class XmppConnectionService extends Service { return this.mJingleConnectionManager; } + public MessageArchiveService getMessageArchiveService() { + return this.mMessageArchiveService; + } + public List findContacts(Jid jid) { ArrayList contacts = new ArrayList<>(); for (Account account : getAccounts()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 02c3e695..c25b9017 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -159,4 +159,9 @@ public class Element { public void setAttribute(String name, int value) { this.setAttribute(name, Integer.toString(value)); } + + public boolean getAttributeAsBoolean(String name) { + String attr = getAttribute(name); + return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesAvailable.java b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesAvailable.java new file mode 100644 index 00000000..a41bce86 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesAvailable.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnAdvancedStreamFeaturesAvailable { + public void onAdvancedStreamFeaturesAvailable(final Account account); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index adb96fa2..0e5d26ed 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -107,6 +107,7 @@ public class XmppConnection implements Runnable { private OnMessagePacketReceived messageListener = null; private OnStatusChanged statusListener = null; private OnBindListener bindListener = null; + private OnAdvancedStreamFeaturesAvailable advancedStreamFeaturesAvailableListener = null; private OnMessageAcknowledged acknowledgedListener = null; private XmppConnectionService mXmppConnectionService = null; @@ -771,6 +772,9 @@ public class XmppConnection implements Runnable { if (account.getServer().equals(server.toDomainJid())) { enableAdvancedStreamFeatures(); + if (advancedStreamFeaturesAvailableListener != null) { + advancedStreamFeaturesAvailableListener.onAdvancedStreamFeaturesAvailable(account); + } } } }); @@ -943,6 +947,10 @@ public class XmppConnection implements Runnable { this.acknowledgedListener = listener; } + public void setOnAdvancedStreamFeaturesAvailableListener(OnAdvancedStreamFeaturesAvailable listener) { + this.advancedStreamFeaturesAvailableListener = listener; + } + public void disconnect(boolean force) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting"); try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index ff9acb3f..44794c80 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -37,6 +37,7 @@ public class Data extends Element { Field field = getFieldByName(name); if (field == null) { field = new Field(name); + this.addChild(field); } field.setValue(value); } @@ -45,6 +46,7 @@ public class Data extends Element { Field field = getFieldByName(name); if (field == null) { field = new Field(name); + this.addChild(field); } field.setValues(values); } @@ -72,4 +74,12 @@ public class Data extends Element { data.setChildren(element.getChildren()); return data; } + + public void setFormType(String formType) { + this.put("FORM_TYPE",formType); + } + + public String getFormType() { + return this.getAttribute("FORM_TYPE"); + } }