1package eu.siacs.conversations.services;
2
3import android.util.Log;
4
5import java.math.BigInteger;
6import java.util.HashSet;
7import java.util.List;
8
9import eu.siacs.conversations.Config;
10import eu.siacs.conversations.entities.Account;
11import eu.siacs.conversations.entities.Conversation;
12import eu.siacs.conversations.generator.AbstractGenerator;
13import eu.siacs.conversations.parser.AbstractParser;
14import eu.siacs.conversations.xml.Element;
15import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
16import eu.siacs.conversations.xmpp.OnIqPacketReceived;
17import eu.siacs.conversations.xmpp.jid.Jid;
18import eu.siacs.conversations.xmpp.stanzas.IqPacket;
19
20public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
21
22 private final XmppConnectionService mXmppConnectionService;
23
24 private final HashSet<Query> queries = new HashSet<Query>();
25
26 public enum PagingOrder {
27 NORMAL,
28 REVERSE
29 };
30
31 public MessageArchiveService(final XmppConnectionService service) {
32 this.mXmppConnectionService = service;
33 }
34
35 public void catchup(final Account account) {
36 long startCatchup = getLastMessageTransmitted(account);
37 long endCatchup = account.getXmppConnection().getLastSessionEstablished();
38 if (startCatchup == 0) {
39 return;
40 } else if (endCatchup - startCatchup >= Config.MAX_CATCHUP) {
41 startCatchup = endCatchup - Config.MAX_CATCHUP;
42 List<Conversation> conversations = mXmppConnectionService.getConversations();
43 for (Conversation conversation : conversations) {
44 if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) {
45 this.query(conversation,startCatchup);
46 }
47 }
48 }
49 final Query query = new Query(account, startCatchup, endCatchup);
50 this.queries.add(query);
51 this.execute(query);
52 }
53
54 private long getLastMessageTransmitted(final Account account) {
55 long timestamp = 0;
56 for(final Conversation conversation : mXmppConnectionService.getConversations()) {
57 if (conversation.getAccount() == account) {
58 long tmp = conversation.getLastMessageTransmitted();
59 if (tmp > timestamp) {
60 timestamp = tmp;
61 }
62 }
63 }
64 return timestamp;
65 }
66
67 public void query(final Conversation conversation) {
68 query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished());
69 }
70
71 public void query(final Conversation conversation, long end) {
72 synchronized (this.queries) {
73 final Account account = conversation.getAccount();
74 long start = conversation.getLastMessageTransmitted();
75 if (start > end) {
76 return;
77 } else if (end - start >= Config.MAX_HISTORY_AGE) {
78 start = end - Config.MAX_HISTORY_AGE;
79 }
80 final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
81 this.queries.add(query);
82 this.execute(query);
83 }
84 }
85
86 private void execute(final Query query) {
87 Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": running mam query "+query.toString());
88 IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
89 this.mXmppConnectionService.sendIqPacket(query.getAccount(), packet, new OnIqPacketReceived() {
90 @Override
91 public void onIqPacketReceived(Account account, IqPacket packet) {
92 if (packet.getType() == IqPacket.TYPE_ERROR) {
93 Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": error executing mam: "+packet.toString());
94 finalizeQuery(query);
95 }
96 }
97 });
98 }
99
100 private void finalizeQuery(Query query) {
101 synchronized (this.queries) {
102 this.queries.remove(query);
103 }
104 final Conversation conversation = query.getConversation();
105 if (conversation != null) {
106 conversation.sort();
107 if (conversation.setLastMessageTransmitted(query.getEnd())) {
108 this.mXmppConnectionService.databaseBackend.updateConversation(conversation);
109 }
110 this.mXmppConnectionService.updateConversationUi();
111 } else {
112 for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
113 if (tmp.getAccount() == query.getAccount()) {
114 tmp.sort();
115 if (tmp.setLastMessageTransmitted(query.getEnd())) {
116 this.mXmppConnectionService.databaseBackend.updateConversation(tmp);
117 }
118 }
119 }
120 }
121 }
122
123 public boolean queryInProgress(Conversation conversation) {
124 synchronized (this.queries) {
125 for(Query query : queries) {
126 if (query.conversation == conversation) {
127 return true;
128 }
129 }
130 return false;
131 }
132 }
133
134 public void processFin(Element fin) {
135 if (fin == null) {
136 return;
137 }
138 Query query = findQuery(fin.getAttribute("queryid"));
139 if (query == null) {
140 return;
141 }
142 boolean complete = fin.getAttributeAsBoolean("complete");
143 Element set = fin.findChild("set","http://jabber.org/protocol/rsm");
144 Element last = set == null ? null : set.findChild("last");
145 Element first = set == null ? null : set.findChild("first");
146 Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
147 if (complete || relevant == null) {
148 this.finalizeQuery(query);
149 } else {
150 final Query nextQuery;
151 if (query.getPagingOrder() == PagingOrder.NORMAL) {
152 nextQuery = query.next(last == null ? null : last.getContent());
153 } else {
154 nextQuery = query.prev(first == null ? null : first.getContent());
155 }
156 this.execute(nextQuery);
157 this.finalizeQuery(query);
158 synchronized (this.queries) {
159 this.queries.remove(query);
160 this.queries.add(nextQuery);
161 }
162 }
163 }
164
165 public Query findQuery(String id) {
166 if (id == null) {
167 return null;
168 }
169 synchronized (this.queries) {
170 for(Query query : this.queries) {
171 if (query.getQueryId().equals(id)) {
172 return query;
173 }
174 }
175 return null;
176 }
177 }
178
179 @Override
180 public void onAdvancedStreamFeaturesAvailable(Account account) {
181 if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
182 this.catchup(account);
183 }
184 }
185
186 public class Query {
187 private long start;
188 private long end;
189 private Jid with = null;
190 private String queryId;
191 private String reference = null;
192 private Account account;
193 private Conversation conversation;
194 private PagingOrder pagingOrder = PagingOrder.NORMAL;
195
196
197 public Query(Conversation conversation, long start, long end) {
198 this(conversation.getAccount(), start, end);
199 this.conversation = conversation;
200 this.with = conversation.getContactJid().toBareJid();
201 }
202
203 public Query(Conversation conversation, long start, long end, PagingOrder order) {
204 this(conversation,start,end);
205 this.pagingOrder = order;
206 }
207
208 public Query(Account account, long start, long end) {
209 this.account = account;
210 this.start = start;
211 this.end = end;
212 this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
213 }
214
215 private Query page(String reference) {
216 Query query = new Query(this.account,this.start,this.end);
217 query.reference = reference;
218 query.conversation = conversation;
219 query.with = with;
220 return query;
221 }
222
223 public Query next(String reference) {
224 Query query = page(reference);
225 query.pagingOrder = PagingOrder.NORMAL;
226 return query;
227 }
228
229 public Query prev(String reference) {
230 Query query = page(reference);
231 query.pagingOrder = PagingOrder.REVERSE;
232 return query;
233 }
234
235 public String getReference() {
236 return reference;
237 }
238
239 public PagingOrder getPagingOrder() {
240 return this.pagingOrder;
241 }
242
243 public String getQueryId() {
244 return queryId;
245 }
246
247 public Jid getWith() {
248 return with;
249 }
250
251 public long getStart() {
252 return start;
253 }
254
255 public long getEnd() {
256 return end;
257 }
258
259 public Conversation getConversation() {
260 return conversation;
261 }
262
263 public Account getAccount() {
264 return this.account;
265 }
266
267 @Override
268 public String toString() {
269 StringBuilder builder = new StringBuilder();
270 builder.append("with=");
271 if (this.with==null) {
272 builder.append("*");
273 } else {
274 builder.append(with.toString());
275 }
276 builder.append(", start=");
277 builder.append(AbstractGenerator.getTimestamp(this.start));
278 builder.append(", end=");
279 builder.append(AbstractGenerator.getTimestamp(this.end));
280 if (this.reference!=null) {
281 if (this.pagingOrder == PagingOrder.NORMAL) {
282 builder.append(", after=");
283 } else {
284 builder.append(", before=");
285 }
286 builder.append(this.reference);
287 }
288 return builder.toString();
289 }
290 }
291}