MessageArchiveService.java

  1package eu.siacs.conversations.services;
  2
  3import android.util.Log;
  4import android.util.Pair;
  5
  6import java.math.BigInteger;
  7import java.util.ArrayList;
  8import java.util.HashSet;
  9import java.util.Iterator;
 10import java.util.List;
 11
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.R;
 14import eu.siacs.conversations.entities.Account;
 15import eu.siacs.conversations.entities.Conversation;
 16import eu.siacs.conversations.generator.AbstractGenerator;
 17import eu.siacs.conversations.xml.Element;
 18import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
 19import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 20import eu.siacs.conversations.xmpp.jid.Jid;
 21import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 22
 23public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 24
 25	private final XmppConnectionService mXmppConnectionService;
 26
 27	private final HashSet<Query> queries = new HashSet<>();
 28	private final ArrayList<Query> pendingQueries = new ArrayList<>();
 29
 30	public enum PagingOrder {
 31		NORMAL,
 32		REVERSE
 33	}
 34
 35	public MessageArchiveService(final XmppConnectionService service) {
 36		this.mXmppConnectionService = service;
 37	}
 38
 39	private void catchup(final Account account) {
 40		synchronized (this.queries) {
 41			for(Iterator<Query> iterator = this.queries.iterator(); iterator.hasNext();) {
 42				Query query = iterator.next();
 43				if (query.getAccount() == account) {
 44					iterator.remove();
 45				}
 46			}
 47		}
 48		final Pair<Long,String> lastMessageReceived = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
 49		final Pair<Long,String> lastClearDate = mXmppConnectionService.databaseBackend.getLastClearDate(account);
 50		long startCatchup;
 51		final String reference;
 52		if (lastMessageReceived != null && lastMessageReceived.first >= lastClearDate.first) {
 53			startCatchup = lastMessageReceived.first;
 54			reference = lastMessageReceived.second;
 55		} else {
 56			startCatchup = lastClearDate.first;
 57			reference = null;
 58		}
 59		startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
 60		long endCatchup = account.getXmppConnection().getLastSessionEstablished();
 61		final Query query;
 62		if (startCatchup == 0) {
 63			return;
 64		} else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) {
 65			startCatchup = endCatchup - Config.MAM_MAX_CATCHUP;
 66			List<Conversation> conversations = mXmppConnectionService.getConversations();
 67			for (Conversation conversation : conversations) {
 68				if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) {
 69					this.query(conversation,startCatchup);
 70				}
 71			}
 72			query = new Query(account, startCatchup, endCatchup);
 73		} else {
 74			query = new Query(account, startCatchup, endCatchup);
 75			query.reference = reference;
 76		}
 77		this.queries.add(query);
 78		this.execute(query);
 79	}
 80
 81	public void catchupMUC(final Conversation conversation) {
 82		if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
 83			query(conversation,
 84					0,
 85					System.currentTimeMillis());
 86		} else {
 87			query(conversation,
 88					conversation.getLastMessageTransmitted(),
 89					System.currentTimeMillis());
 90		}
 91	}
 92
 93	public Query query(final Conversation conversation) {
 94		if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
 95			return query(conversation,
 96					0,
 97					System.currentTimeMillis());
 98		} else {
 99			return query(conversation,
100					conversation.getLastMessageTransmitted(),
101					conversation.getAccount().getXmppConnection().getLastSessionEstablished());
102		}
103	}
104
105	public Query query(final Conversation conversation, long end) {
106		return this.query(conversation,conversation.getLastMessageTransmitted(),end);
107	}
108
109	public Query query(Conversation conversation, long start, long end) {
110		synchronized (this.queries) {
111			final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
112			if (start==0) {
113				query.reference = conversation.getFirstMamReference();
114				Log.d(Config.LOGTAG,"setting mam reference");
115			}
116			query.start = Math.max(start,mXmppConnectionService.getAutomaticMessageDeletionDate());
117			if (start > end) {
118				return null;
119			}
120			this.queries.add(query);
121			this.execute(query);
122			return query;
123		}
124	}
125
126	public void executePendingQueries(final Account account) {
127		List<Query> pending = new ArrayList<>();
128		synchronized(this.pendingQueries) {
129			for(Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext();) {
130				Query query = iterator.next();
131				if (query.getAccount() == account) {
132					pending.add(query);
133					iterator.remove();
134				}
135			}
136		}
137		for(Query query : pending) {
138			this.execute(query);
139		}
140	}
141
142	private void execute(final Query query) {
143		final Account account=  query.getAccount();
144		if (account.getStatus() == Account.State.ONLINE) {
145			Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString());
146			IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
147			this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
148				@Override
149				public void onIqPacketReceived(Account account, IqPacket packet) {
150					if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
151						synchronized (MessageArchiveService.this.queries) {
152							MessageArchiveService.this.queries.remove(query);
153							if (query.hasCallback()) {
154								query.callback(false);
155							}
156						}
157					} else if (packet.getType() != IqPacket.TYPE.RESULT) {
158						Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
159						finalizeQuery(query, true);
160					}
161				}
162			});
163		} else {
164			synchronized (this.pendingQueries) {
165				this.pendingQueries.add(query);
166			}
167		}
168	}
169
170	private void finalizeQuery(Query query, boolean done) {
171		synchronized (this.queries) {
172			this.queries.remove(query);
173		}
174		final Conversation conversation = query.getConversation();
175		if (conversation != null) {
176			conversation.sort();
177			conversation.setHasMessagesLeftOnServer(!done);
178		} else {
179			for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
180				if (tmp.getAccount() == query.getAccount()) {
181					tmp.sort();
182				}
183			}
184		}
185		if (query.hasCallback()) {
186			query.callback(done);
187		} else {
188			this.mXmppConnectionService.updateConversationUi();
189		}
190	}
191
192	public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
193		synchronized (this.queries) {
194			for(Query query : queries) {
195				if (query.conversation == conversation) {
196					if (!query.hasCallback() && callback != null) {
197						query.setCallback(callback);
198					}
199					return true;
200				}
201			}
202			return false;
203		}
204	}
205
206	public boolean queryInProgress(Conversation conversation) {
207		return queryInProgress(conversation, null);
208	}
209
210	public void processFin(Element fin, Jid from) {
211		if (fin == null) {
212			return;
213		}
214		Query query = findQuery(fin.getAttribute("queryid"));
215		if (query == null || !query.validFrom(from)) {
216			return;
217		}
218		boolean complete = fin.getAttributeAsBoolean("complete");
219		Element set = fin.findChild("set","http://jabber.org/protocol/rsm");
220		Element last = set == null ? null : set.findChild("last");
221		Element first = set == null ? null : set.findChild("first");
222		Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
223		boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
224		if (query.getConversation() != null) {
225			query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
226		}
227		if (complete || relevant == null || abort) {
228			final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() <= mXmppConnectionService.getAutomaticMessageDeletionDate();
229			this.finalizeQuery(query, done);
230			Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done));
231			if (query.getWith() == null && query.getMessageCount() > 0) {
232				mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount());
233			}
234		} else {
235			final Query nextQuery;
236			if (query.getPagingOrder() == PagingOrder.NORMAL) {
237				nextQuery = query.next(last == null ? null : last.getContent());
238			} else {
239				nextQuery = query.prev(first == null ? null : first.getContent());
240			}
241			this.execute(nextQuery);
242			this.finalizeQuery(query, false);
243			synchronized (this.queries) {
244				this.queries.add(nextQuery);
245			}
246		}
247	}
248
249	public Query findQuery(String id) {
250		if (id == null) {
251			return null;
252		}
253		synchronized (this.queries) {
254			for(Query query : this.queries) {
255				if (query.getQueryId().equals(id)) {
256					return query;
257				}
258			}
259			return null;
260		}
261	}
262
263	@Override
264	public void onAdvancedStreamFeaturesAvailable(Account account) {
265		if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
266			this.catchup(account);
267		}
268	}
269
270	public class Query {
271		private int totalCount = 0;
272		private int messageCount = 0;
273		private long start;
274		private long end;
275		private String queryId;
276		private String reference = null;
277		private Account account;
278		private Conversation conversation;
279		private PagingOrder pagingOrder = PagingOrder.NORMAL;
280		private XmppConnectionService.OnMoreMessagesLoaded callback = null;
281
282
283		public Query(Conversation conversation, long start, long end) {
284			this(conversation.getAccount(), start, end);
285			this.conversation = conversation;
286		}
287
288		public Query(Conversation conversation, long start, long end, PagingOrder order) {
289			this(conversation,start,end);
290			this.pagingOrder = order;
291		}
292
293		public Query(Account account, long start, long end) {
294			this.account = account;
295			this.start = start;
296			this.end = end;
297			this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
298		}
299		
300		private Query page(String reference) {
301			Query query = new Query(this.account,this.start,this.end);
302			query.reference = reference;
303			query.conversation = conversation;
304			query.totalCount = totalCount;
305			query.callback = callback;
306			return query;
307		}
308
309		public Query next(String reference) {
310			Query query = page(reference);
311			query.pagingOrder = PagingOrder.NORMAL;
312			return query;
313		}
314
315		public Query prev(String reference) {
316			Query query = page(reference);
317			query.pagingOrder = PagingOrder.REVERSE;
318			return query;
319		}
320
321		public String getReference() {
322			return reference;
323		}
324
325		public PagingOrder getPagingOrder() {
326			return this.pagingOrder;
327		}
328
329		public String getQueryId() {
330			return queryId;
331		}
332
333		public Jid getWith() {
334			return conversation == null ? null : conversation.getJid().toBareJid();
335		}
336
337		public boolean muc() {
338			return conversation != null && conversation.getMode() == Conversation.MODE_MULTI;
339		}
340
341		public long getStart() {
342			return start;
343		}
344
345		public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) {
346			this.callback = callback;
347		}
348
349		public void callback(boolean done) {
350			if (this.callback != null) {
351				this.callback.onMoreMessagesLoaded(messageCount,conversation);
352				if (done) {
353					this.callback.informUser(R.string.no_more_history_on_server);
354				}
355			}
356		}
357
358		public long getEnd() {
359			return end;
360		}
361
362		public Conversation getConversation() {
363			return conversation;
364		}
365
366		public Account getAccount() {
367			return this.account;
368		}
369
370		public void incrementMessageCount() {
371			this.messageCount++;
372			this.totalCount++;
373		}
374
375		public int getTotalCount() {
376			return this.totalCount;
377		}
378
379		public int getMessageCount() {
380			return this.messageCount;
381		}
382
383		public boolean validFrom(Jid from) {
384			if (muc()) {
385				return getWith().equals(from);
386			} else {
387				return (from == null) || account.getJid().toBareJid().equals(from.toBareJid());
388			}
389		}
390
391		@Override
392		public String toString() {
393			StringBuilder builder = new StringBuilder();
394			if (this.muc()) {
395				builder.append("to=");
396				builder.append(this.getWith().toString());
397			} else {
398				builder.append("with=");
399				if (this.getWith() == null) {
400					builder.append("*");
401				} else {
402					builder.append(getWith().toString());
403				}
404			}
405			builder.append(", start=");
406			builder.append(AbstractGenerator.getTimestamp(this.start));
407			builder.append(", end=");
408			builder.append(AbstractGenerator.getTimestamp(this.end));
409			if (this.reference!=null) {
410				if (this.pagingOrder == PagingOrder.NORMAL) {
411					builder.append(", after=");
412				} else {
413					builder.append(", before=");
414				}
415				builder.append(this.reference);
416			}
417			return builder.toString();
418		}
419
420		public boolean hasCallback() {
421			return this.callback != null;
422		}
423	}
424}