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.MESSAGE_MERGE_WINDOW * 1000;
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.treatAsDownloadable() == Decision.NO &&
379 this.treatAsDownloadable() == Decision.NO &&
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 enum Decision {
438 YES,
439 NO,
440 ASK
441 }
442
443 public Decision treatAsDownloadable() {
444 if (body.trim().contains(" ")) {
445 return Decision.NO;
446 }
447 try {
448 URL url = new URL(body);
449 if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
450 return Decision.NO;
451 }
452 String path = url.getPath();
453 if (path == null || path.isEmpty()) {
454 return Decision.NO;
455 }
456
457 String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
458 String[] extensionParts = filename.split("\\.");
459 String extension;
460 String ref = url.getRef();
461 if (extensionParts.length == 2) {
462 extension = extensionParts[extensionParts.length - 1];
463 } else if (extensionParts.length == 3 && Arrays
464 .asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
465 .contains(extensionParts[extensionParts.length - 1])) {
466 extension = extensionParts[extensionParts.length -2];
467 } else {
468 return Decision.NO;
469 }
470
471 if (Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(extension)) {
472 return Decision.YES;
473 } else if (ref != null && ref.matches("([A-Fa-f0-9]{2}){48}")) {
474 return Decision.ASK;
475 } else {
476 return Decision.NO;
477 }
478
479 } catch (MalformedURLException e) {
480 return Decision.NO;
481 }
482 }
483
484 public boolean bodyIsHeart() {
485 return body != null && UIHelper.HEARTS.contains(body.trim());
486 }
487
488 public FileParams getFileParams() {
489 FileParams params = getLegacyFileParams();
490 if (params != null) {
491 return params;
492 }
493 params = new FileParams();
494 if (this.downloadable != null) {
495 params.size = this.downloadable.getFileSize();
496 }
497 if (body == null) {
498 return params;
499 }
500 String parts[] = body.split("\\|");
501 switch (parts.length) {
502 case 1:
503 try {
504 params.size = Long.parseLong(parts[0]);
505 } catch (NumberFormatException e) {
506 try {
507 params.url = new URL(parts[0]);
508 } catch (MalformedURLException e1) {
509 params.url = null;
510 }
511 }
512 break;
513 case 2:
514 case 4:
515 try {
516 params.url = new URL(parts[0]);
517 } catch (MalformedURLException e1) {
518 params.url = null;
519 }
520 try {
521 params.size = Long.parseLong(parts[1]);
522 } catch (NumberFormatException e) {
523 params.size = 0;
524 }
525 try {
526 params.width = Integer.parseInt(parts[2]);
527 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
528 params.width = 0;
529 }
530 try {
531 params.height = Integer.parseInt(parts[3]);
532 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
533 params.height = 0;
534 }
535 break;
536 case 3:
537 try {
538 params.size = Long.parseLong(parts[0]);
539 } catch (NumberFormatException e) {
540 params.size = 0;
541 }
542 try {
543 params.width = Integer.parseInt(parts[1]);
544 } catch (NumberFormatException e) {
545 params.width = 0;
546 }
547 try {
548 params.height = Integer.parseInt(parts[2]);
549 } catch (NumberFormatException e) {
550 params.height = 0;
551 }
552 break;
553 }
554 return params;
555 }
556
557 public FileParams getLegacyFileParams() {
558 FileParams params = new FileParams();
559 if (body == null) {
560 return params;
561 }
562 String parts[] = body.split(",");
563 if (parts.length == 3) {
564 try {
565 params.size = Long.parseLong(parts[0]);
566 } catch (NumberFormatException e) {
567 return null;
568 }
569 try {
570 params.width = Integer.parseInt(parts[1]);
571 } catch (NumberFormatException e) {
572 return null;
573 }
574 try {
575 params.height = Integer.parseInt(parts[2]);
576 } catch (NumberFormatException e) {
577 return null;
578 }
579 return params;
580 } else {
581 return null;
582 }
583 }
584
585 public void untie() {
586 this.mNextMessage = null;
587 this.mPreviousMessage = null;
588 }
589
590 public boolean isFileOrImage() {
591 return type == TYPE_FILE || type == TYPE_IMAGE;
592 }
593
594 public boolean hasFileOnRemoteHost() {
595 return isFileOrImage() && getFileParams().url != null;
596 }
597
598 public boolean needsUploading() {
599 return isFileOrImage() && getFileParams().url == null;
600 }
601
602 public class FileParams {
603 public URL url;
604 public long size = 0;
605 public int width = 0;
606 public int height = 0;
607 }
608}