very basic mam support

iNPUTmice created

Change summary

src/main/java/eu/siacs/conversations/entities/Bookmark.java                      |   4 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                  |  18 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                   |  66 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java         | 137 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java         |  30 
src/main/java/eu/siacs/conversations/xml/Element.java                            |   5 
src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesAvailable.java |   7 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                    |   8 
src/main/java/eu/siacs/conversations/xmpp/forms/Data.java                        |  10 
9 files changed, 277 insertions(+), 8 deletions(-)

Detailed changes

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() {

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;
+	}
 }

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();

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<Query> queries = new HashSet<Query>();
+
+	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;
+		}
+	}
+}

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<Conversation> 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<Contact> findContacts(Jid jid) {
 		ArrayList<Contact> contacts = new ArrayList<>();
 		for (Account account : getAccounts()) {

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")));
+	}
 }

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 {

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");
+	}
 }