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