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}