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 MessageArchiveService(final XmppConnectionService service) {
27 this.mXmppConnectionService = service;
28 }
29
30 public void catchup(final Account account) {
31 long startCatchup = getLastMessageTransmitted(account);
32 long endCatchup = account.getXmppConnection().getLastSessionEstablished();
33 if (startCatchup == 0) {
34 return;
35 } else if (endCatchup - startCatchup >= Config.MAX_CATCHUP) {
36 startCatchup = endCatchup - Config.MAX_CATCHUP;
37 List<Conversation> conversations = mXmppConnectionService.getConversations();
38 for (Conversation conversation : conversations) {
39 if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) {
40 this.query(conversation,startCatchup);
41 }
42 }
43 }
44 final Query query = new Query(account, startCatchup, endCatchup);
45 this.queries.add(query);
46 this.execute(query);
47 }
48
49 private long getLastMessageTransmitted(final Account account) {
50 long timestamp = 0;
51 for(final Conversation conversation : mXmppConnectionService.getConversations()) {
52 if (conversation.getAccount() == account) {
53 long tmp = conversation.getLastMessageTransmitted();
54 if (tmp > timestamp) {
55 timestamp = tmp;
56 }
57 }
58 }
59 return timestamp;
60 }
61
62 public void query(final Conversation conversation) {
63 query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished());
64 }
65
66 public void query(final Conversation conversation, long end) {
67 synchronized (this.queries) {
68 final Account account = conversation.getAccount();
69 long start = conversation.getLastMessageTransmitted();
70 if (start > end) {
71 return;
72 } else if (end - start >= Config.MAX_HISTORY_AGE) {
73 start = end - Config.MAX_HISTORY_AGE;
74 }
75 final Query query = new Query(conversation, start, end);
76 this.queries.add(query);
77 this.execute(query);
78 }
79 }
80
81 private void execute(final Query query) {
82 Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": running mam query "+query.toString());
83 IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
84 this.mXmppConnectionService.sendIqPacket(query.getAccount(), packet, new OnIqPacketReceived() {
85 @Override
86 public void onIqPacketReceived(Account account, IqPacket packet) {
87 if (packet.getType() == IqPacket.TYPE_ERROR) {
88 Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": error executing mam: "+packet.toString());
89 finalizeQuery(query);
90 }
91 }
92 });
93 }
94
95 private void finalizeQuery(Query query) {
96 synchronized (this.queries) {
97 this.queries.remove(query);
98 }
99 final Conversation conversation = query.getConversation();
100 if (conversation != null) {
101 conversation.sort();
102 if (conversation.setLastMessageTransmitted(query.getEnd())) {
103 this.mXmppConnectionService.databaseBackend.updateConversation(conversation);
104 }
105 this.mXmppConnectionService.updateConversationUi();
106 } else {
107 for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
108 if (tmp.getAccount() == query.getAccount()) {
109 tmp.sort();
110 if (tmp.setLastMessageTransmitted(query.getEnd())) {
111 this.mXmppConnectionService.databaseBackend.updateConversation(tmp);
112 }
113 }
114 }
115 }
116 }
117
118 public void processFin(Element fin) {
119 if (fin == null) {
120 return;
121 }
122 Query query = findQuery(fin.getAttribute("queryid"));
123 if (query == null) {
124 return;
125 }
126 boolean complete = fin.getAttributeAsBoolean("complete");
127 Element set = fin.findChild("set","http://jabber.org/protocol/rsm");
128 Element last = set == null ? null : set.findChild("last");
129 if (complete || last == null) {
130 this.finalizeQuery(query);
131 } else {
132 final Query nextQuery = query.next(last == null ? null : last.getContent());
133 this.execute(nextQuery);
134 synchronized (this.queries) {
135 this.queries.remove(query);
136 this.queries.add(nextQuery);
137 }
138 }
139 }
140
141 public Query findQuery(String id) {
142 if (id == null) {
143 return null;
144 }
145 synchronized (this.queries) {
146 for(Query query : this.queries) {
147 if (query.getQueryId().equals(id)) {
148 return query;
149 }
150 }
151 return null;
152 }
153 }
154
155 @Override
156 public void onAdvancedStreamFeaturesAvailable(Account account) {
157 if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
158 this.catchup(account);
159 }
160 }
161
162 public class Query {
163 private long start;
164 private long end;
165 private Jid with = null;
166 private String queryId;
167 private String after = null;
168 private Account account;
169 private Conversation conversation;
170
171 public Query(Conversation conversation, long start, long end) {
172 this(conversation.getAccount(), start, end);
173 this.conversation = conversation;
174 this.with = conversation.getContactJid().toBareJid();
175 }
176
177 public Query(Account account, long start, long end) {
178 this.account = account;
179 this.start = start;
180 this.end = end;
181 this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
182 }
183
184 public Query next(String after) {
185 Query query = new Query(this.account,this.start,this.end);
186 query.after = after;
187 query.conversation = conversation;
188 query.with = with;
189 return query;
190 }
191
192 public String getAfter() {
193 return after;
194 }
195
196 public String getQueryId() {
197 return queryId;
198 }
199
200 public Jid getWith() {
201 return with;
202 }
203
204 public long getStart() {
205 return start;
206 }
207
208 public long getEnd() {
209 return end;
210 }
211
212 public Conversation getConversation() {
213 return conversation;
214 }
215
216 public Account getAccount() {
217 return this.account;
218 }
219
220 @Override
221 public String toString() {
222 StringBuilder builder = new StringBuilder();
223 builder.append("with=");
224 if (this.with==null) {
225 builder.append("*");
226 } else {
227 builder.append(with.toString());
228 }
229 builder.append(", start=");
230 builder.append(AbstractGenerator.getTimestamp(this.start));
231 builder.append(", end=");
232 builder.append(AbstractGenerator.getTimestamp(this.end));
233 if (this.after!=null) {
234 builder.append(", after=");
235 builder.append(this.after);
236 }
237 return builder.toString();
238 }
239 }
240}