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