1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import java.net.MalformedURLException;
7import java.net.URL;
8import java.util.Arrays;
9
10import eu.siacs.conversations.Config;
11import eu.siacs.conversations.xmpp.jid.InvalidJidException;
12import eu.siacs.conversations.xmpp.jid.Jid;
13
14public class Message extends AbstractEntity {
15
16 public static final String TABLENAME = "messages";
17
18 public static final int STATUS_RECEIVED = 0;
19 public static final int STATUS_UNSEND = 1;
20 public static final int STATUS_SEND = 2;
21 public static final int STATUS_SEND_FAILED = 3;
22 public static final int STATUS_WAITING = 5;
23 public static final int STATUS_OFFERED = 6;
24 public static final int STATUS_SEND_RECEIVED = 7;
25 public static final int STATUS_SEND_DISPLAYED = 8;
26
27 public static final int ENCRYPTION_NONE = 0;
28 public static final int ENCRYPTION_PGP = 1;
29 public static final int ENCRYPTION_OTR = 2;
30 public static final int ENCRYPTION_DECRYPTED = 3;
31 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
32
33 public static final int TYPE_TEXT = 0;
34 public static final int TYPE_IMAGE = 1;
35 public static final int TYPE_FILE = 2;
36 public static final int TYPE_STATUS = 3;
37 public static final int TYPE_PRIVATE = 4;
38
39 public static final String CONVERSATION = "conversationUuid";
40 public static final String COUNTERPART = "counterpart";
41 public static final String TRUE_COUNTERPART = "trueCounterpart";
42 public static final String BODY = "body";
43 public static final String TIME_SENT = "timeSent";
44 public static final String ENCRYPTION = "encryption";
45 public static final String STATUS = "status";
46 public static final String TYPE = "type";
47 public static final String REMOTE_MSG_ID = "remoteMsgId";
48 public static final String SERVER_MSG_ID = "serverMsgId";
49 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
50
51 public boolean markable = false;
52 protected String conversationUuid;
53 protected Jid counterpart;
54 protected Jid trueCounterpart;
55 protected String body;
56 protected String encryptedBody;
57 protected long timeSent;
58 protected int encryption;
59 protected int status;
60 protected int type;
61 protected String relativeFilePath;
62 protected boolean read = true;
63 protected String remoteMsgId = null;
64 protected String serverMsgId = null;
65 protected Conversation conversation = null;
66 protected Downloadable downloadable = null;
67 private Message mNextMessage = null;
68 private Message mPreviousMessage = null;
69
70 private Message() {
71
72 }
73
74 public Message(Conversation conversation, String body, int encryption) {
75 this(conversation, body, encryption, STATUS_UNSEND);
76 }
77
78 public Message(Conversation conversation, String body, int encryption, int status) {
79 this(java.util.UUID.randomUUID().toString(),
80 conversation.getUuid(),
81 conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
82 null,
83 body,
84 System.currentTimeMillis(),
85 encryption,
86 status,
87 TYPE_TEXT,
88 null,
89 null,
90 null);
91 this.conversation = conversation;
92 }
93
94 private Message(final String uuid, final String conversationUUid, final Jid counterpart,
95 final Jid trueCounterpart, final String body, final long timeSent,
96 final int encryption, final int status, final int type, final String remoteMsgId,
97 final String relativeFilePath, final String serverMsgId) {
98 this.uuid = uuid;
99 this.conversationUuid = conversationUUid;
100 this.counterpart = counterpart;
101 this.trueCounterpart = trueCounterpart;
102 this.body = body;
103 this.timeSent = timeSent;
104 this.encryption = encryption;
105 this.status = status;
106 this.type = type;
107 this.remoteMsgId = remoteMsgId;
108 this.relativeFilePath = relativeFilePath;
109 this.serverMsgId = serverMsgId;
110 }
111
112 public static Message fromCursor(Cursor cursor) {
113 Jid jid;
114 try {
115 String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
116 if (value != null) {
117 jid = Jid.fromString(value);
118 } else {
119 jid = null;
120 }
121 } catch (InvalidJidException e) {
122 jid = null;
123 }
124 Jid trueCounterpart;
125 try {
126 String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
127 if (value != null) {
128 trueCounterpart = Jid.fromString(value);
129 } else {
130 trueCounterpart = null;
131 }
132 } catch (InvalidJidException e) {
133 trueCounterpart = null;
134 }
135 return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
136 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
137 jid,
138 trueCounterpart,
139 cursor.getString(cursor.getColumnIndex(BODY)),
140 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
141 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
142 cursor.getInt(cursor.getColumnIndex(STATUS)),
143 cursor.getInt(cursor.getColumnIndex(TYPE)),
144 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
145 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
146 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
147 }
148
149 public static Message createStatusMessage(Conversation conversation) {
150 Message message = new Message();
151 message.setType(Message.TYPE_STATUS);
152 message.setConversation(conversation);
153 return message;
154 }
155
156 @Override
157 public ContentValues getContentValues() {
158 ContentValues values = new ContentValues();
159 values.put(UUID, uuid);
160 values.put(CONVERSATION, conversationUuid);
161 if (counterpart == null) {
162 values.putNull(COUNTERPART);
163 } else {
164 values.put(COUNTERPART, counterpart.toString());
165 }
166 if (trueCounterpart == null) {
167 values.putNull(TRUE_COUNTERPART);
168 } else {
169 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
170 }
171 values.put(BODY, body);
172 values.put(TIME_SENT, timeSent);
173 values.put(ENCRYPTION, encryption);
174 values.put(STATUS, status);
175 values.put(TYPE, type);
176 values.put(REMOTE_MSG_ID, remoteMsgId);
177 values.put(RELATIVE_FILE_PATH, relativeFilePath);
178 values.put(SERVER_MSG_ID,serverMsgId);
179 return values;
180 }
181
182 public String getConversationUuid() {
183 return conversationUuid;
184 }
185
186 public Conversation getConversation() {
187 return this.conversation;
188 }
189
190 public void setConversation(Conversation conv) {
191 this.conversation = conv;
192 }
193
194 public Jid getCounterpart() {
195 return counterpart;
196 }
197
198 public void setCounterpart(final Jid counterpart) {
199 this.counterpart = counterpart;
200 }
201
202 public Contact getContact() {
203 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
204 return this.conversation.getContact();
205 } else {
206 if (this.trueCounterpart == null) {
207 return null;
208 } else {
209 return this.conversation.getAccount().getRoster()
210 .getContactFromRoster(this.trueCounterpart);
211 }
212 }
213 }
214
215 public String getBody() {
216 return body;
217 }
218
219 public void setBody(String body) {
220 this.body = body;
221 }
222
223 public long getTimeSent() {
224 return timeSent;
225 }
226
227 public int getEncryption() {
228 return encryption;
229 }
230
231 public void setEncryption(int encryption) {
232 this.encryption = encryption;
233 }
234
235 public int getStatus() {
236 return status;
237 }
238
239 public void setStatus(int status) {
240 this.status = status;
241 }
242
243 public String getRelativeFilePath() {
244 return this.relativeFilePath;
245 }
246
247 public void setRelativeFilePath(String path) {
248 this.relativeFilePath = path;
249 }
250
251 public String getRemoteMsgId() {
252 return this.remoteMsgId;
253 }
254
255 public void setRemoteMsgId(String id) {
256 this.remoteMsgId = id;
257 }
258
259 public String getServerMsgId() {
260 return this.serverMsgId;
261 }
262
263 public void setServerMsgId(String id) {
264 this.serverMsgId = id;
265 }
266
267 public boolean isRead() {
268 return this.read;
269 }
270
271 public void markRead() {
272 this.read = true;
273 }
274
275 public void markUnread() {
276 this.read = false;
277 }
278
279 public void setTime(long time) {
280 this.timeSent = time;
281 }
282
283 public String getEncryptedBody() {
284 return this.encryptedBody;
285 }
286
287 public void setEncryptedBody(String body) {
288 this.encryptedBody = body;
289 }
290
291 public int getType() {
292 return this.type;
293 }
294
295 public void setType(int type) {
296 this.type = type;
297 }
298
299 public void setTrueCounterpart(Jid trueCounterpart) {
300 this.trueCounterpart = trueCounterpart;
301 }
302
303 public Downloadable getDownloadable() {
304 return this.downloadable;
305 }
306
307 public void setDownloadable(Downloadable downloadable) {
308 this.downloadable = downloadable;
309 }
310
311 public boolean equals(Message message) {
312 if (this.serverMsgId != null && message.getServerMsgId() != null) {
313 return this.serverMsgId.equals(message.getServerMsgId());
314 } else if (this.body == null || this.counterpart == null) {
315 return false;
316 } else if (message.getRemoteMsgId() != null) {
317 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
318 && this.counterpart.equals(message.getCounterpart())
319 && this.body.equals(message.getBody());
320 } else {
321 return this.remoteMsgId == null
322 && this.counterpart.equals(message.getCounterpart())
323 && this.body.equals(message.getBody())
324 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500;
325 }
326 }
327
328 public Message next() {
329 synchronized (this.conversation.messages) {
330 if (this.mNextMessage == null) {
331 int index = this.conversation.messages.indexOf(this);
332 if (index < 0 || index >= this.conversation.messages.size() - 1) {
333 this.mNextMessage = null;
334 } else {
335 this.mNextMessage = this.conversation.messages.get(index + 1);
336 }
337 }
338 return this.mNextMessage;
339 }
340 }
341
342 public Message prev() {
343 synchronized (this.conversation.messages) {
344 if (this.mPreviousMessage == null) {
345 int index = this.conversation.messages.indexOf(this);
346 if (index <= 0 || index > this.conversation.messages.size()) {
347 this.mPreviousMessage = null;
348 } else {
349 this.mPreviousMessage = this.conversation.messages.get(index - 1);
350 }
351 }
352 return this.mPreviousMessage;
353 }
354 }
355
356 public boolean mergeable(final Message message) {
357 return message != null &&
358 (message.getType() == Message.TYPE_TEXT &&
359 this.getDownloadable() == null &&
360 message.getDownloadable() == null &&
361 message.getEncryption() != Message.ENCRYPTION_PGP &&
362 this.getType() == message.getType() &&
363 this.getStatus() == message.getStatus() &&
364 this.getEncryption() == message.getEncryption() &&
365 this.getCounterpart() != null &&
366 this.getCounterpart().equals(message.getCounterpart()) &&
367 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
368 !message.bodyContainsDownloadable() &&
369 !this.bodyContainsDownloadable() &&
370 !this.body.startsWith("/me ")
371 );
372 }
373
374 public String getMergedBody() {
375 final Message next = this.next();
376 if (this.mergeable(next)) {
377 return getBody() + '\n' + next.getMergedBody();
378 }
379 return getBody();
380 }
381
382 public boolean hasMeCommand() {
383 return getMergedBody().startsWith("/me ");
384 }
385
386 public int getMergedStatus() {
387 return getStatus();
388 }
389
390 public long getMergedTimeSent() {
391 Message next = this.next();
392 if (this.mergeable(next)) {
393 return next.getMergedTimeSent();
394 } else {
395 return getTimeSent();
396 }
397 }
398
399 public boolean wasMergedIntoPrevious() {
400 Message prev = this.prev();
401 return prev != null && prev.mergeable(this);
402 }
403
404 public boolean trusted() {
405 Contact contact = this.getContact();
406 return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
407 }
408
409 public boolean bodyContainsDownloadable() {
410 try {
411 URL url = new URL(this.getBody());
412 if (!url.getProtocol().equalsIgnoreCase("http")
413 && !url.getProtocol().equalsIgnoreCase("https")) {
414 return false;
415 }
416 if (url.getPath() == null) {
417 return false;
418 }
419 String[] pathParts = url.getPath().split("/");
420 String filename;
421 if (pathParts.length > 0) {
422 filename = pathParts[pathParts.length - 1].toLowerCase();
423 } else {
424 return false;
425 }
426 String[] extensionParts = filename.split("\\.");
427 if (extensionParts.length == 2
428 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
429 extensionParts[extensionParts.length - 1])) {
430 return true;
431 } else if (extensionParts.length == 3
432 && Arrays
433 .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
434 .contains(extensionParts[extensionParts.length - 1])
435 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
436 extensionParts[extensionParts.length - 2])) {
437 return true;
438 } else {
439 return false;
440 }
441 } catch (MalformedURLException e) {
442 return false;
443 }
444 }
445
446 public ImageParams getImageParams() {
447 ImageParams params = getLegacyImageParams();
448 if (params != null) {
449 return params;
450 }
451 params = new ImageParams();
452 if (this.downloadable != null) {
453 params.size = this.downloadable.getFileSize();
454 }
455 if (body == null) {
456 return params;
457 }
458 String parts[] = body.split("\\|");
459 if (parts.length == 1) {
460 try {
461 params.size = Long.parseLong(parts[0]);
462 } catch (NumberFormatException e) {
463 params.origin = parts[0];
464 try {
465 params.url = new URL(parts[0]);
466 } catch (MalformedURLException e1) {
467 params.url = null;
468 }
469 }
470 } else if (parts.length == 3) {
471 try {
472 params.size = Long.parseLong(parts[0]);
473 } catch (NumberFormatException e) {
474 params.size = 0;
475 }
476 try {
477 params.width = Integer.parseInt(parts[1]);
478 } catch (NumberFormatException e) {
479 params.width = 0;
480 }
481 try {
482 params.height = Integer.parseInt(parts[2]);
483 } catch (NumberFormatException e) {
484 params.height = 0;
485 }
486 } else if (parts.length == 4) {
487 params.origin = parts[0];
488 try {
489 params.url = new URL(parts[0]);
490 } catch (MalformedURLException e1) {
491 params.url = null;
492 }
493 try {
494 params.size = Long.parseLong(parts[1]);
495 } catch (NumberFormatException e) {
496 params.size = 0;
497 }
498 try {
499 params.width = Integer.parseInt(parts[2]);
500 } catch (NumberFormatException e) {
501 params.width = 0;
502 }
503 try {
504 params.height = Integer.parseInt(parts[3]);
505 } catch (NumberFormatException e) {
506 params.height = 0;
507 }
508 }
509 return params;
510 }
511
512 public ImageParams getLegacyImageParams() {
513 ImageParams params = new ImageParams();
514 if (body == null) {
515 return params;
516 }
517 String parts[] = body.split(",");
518 if (parts.length == 3) {
519 try {
520 params.size = Long.parseLong(parts[0]);
521 } catch (NumberFormatException e) {
522 return null;
523 }
524 try {
525 params.width = Integer.parseInt(parts[1]);
526 } catch (NumberFormatException e) {
527 return null;
528 }
529 try {
530 params.height = Integer.parseInt(parts[2]);
531 } catch (NumberFormatException e) {
532 return null;
533 }
534 return params;
535 } else {
536 return null;
537 }
538 }
539
540 public void untie() {
541 this.mNextMessage = null;
542 this.mPreviousMessage = null;
543 }
544
545 public boolean isFileOrImage() {
546 return type == TYPE_FILE || type == TYPE_IMAGE;
547 }
548
549 public class ImageParams {
550 public URL url;
551 public long size = 0;
552 public int width = 0;
553 public int height = 0;
554 public String origin;
555 }
556}