1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.graphics.Color;
6import android.text.SpannableStringBuilder;
7import android.util.Log;
8
9import org.json.JSONException;
10
11import java.lang.ref.WeakReference;
12import java.net.MalformedURLException;
13import java.net.URL;
14import java.util.ArrayList;
15import java.util.Collections;
16import java.util.HashSet;
17import java.util.Iterator;
18import java.util.List;
19import java.util.Set;
20
21import eu.siacs.conversations.Config;
22import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
23import eu.siacs.conversations.services.AvatarService;
24import eu.siacs.conversations.utils.CryptoHelper;
25import eu.siacs.conversations.utils.Emoticons;
26import eu.siacs.conversations.utils.GeoHelper;
27import eu.siacs.conversations.utils.MessageUtils;
28import eu.siacs.conversations.utils.MimeUtils;
29import eu.siacs.conversations.utils.UIHelper;
30import eu.siacs.conversations.xmpp.Jid;
31
32public class Message extends AbstractEntity implements AvatarService.Avatarable {
33
34 public static final String TABLENAME = "messages";
35
36 public static final int STATUS_RECEIVED = 0;
37 public static final int STATUS_UNSEND = 1;
38 public static final int STATUS_SEND = 2;
39 public static final int STATUS_SEND_FAILED = 3;
40 public static final int STATUS_WAITING = 5;
41 public static final int STATUS_OFFERED = 6;
42 public static final int STATUS_SEND_RECEIVED = 7;
43 public static final int STATUS_SEND_DISPLAYED = 8;
44
45 public static final int ENCRYPTION_NONE = 0;
46 public static final int ENCRYPTION_PGP = 1;
47 public static final int ENCRYPTION_OTR = 2;
48 public static final int ENCRYPTION_DECRYPTED = 3;
49 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
50 public static final int ENCRYPTION_AXOLOTL = 5;
51 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
52 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
53
54 public static final int TYPE_TEXT = 0;
55 public static final int TYPE_IMAGE = 1;
56 public static final int TYPE_FILE = 2;
57 public static final int TYPE_STATUS = 3;
58 public static final int TYPE_PRIVATE = 4;
59 public static final int TYPE_PRIVATE_FILE = 5;
60 public static final int TYPE_RTP_SESSION = 6;
61
62 public static final String CONVERSATION = "conversationUuid";
63 public static final String COUNTERPART = "counterpart";
64 public static final String TRUE_COUNTERPART = "trueCounterpart";
65 public static final String BODY = "body";
66 public static final String BODY_LANGUAGE = "bodyLanguage";
67 public static final String TIME_SENT = "timeSent";
68 public static final String ENCRYPTION = "encryption";
69 public static final String STATUS = "status";
70 public static final String TYPE = "type";
71 public static final String CARBON = "carbon";
72 public static final String OOB = "oob";
73 public static final String EDITED = "edited";
74 public static final String REMOTE_MSG_ID = "remoteMsgId";
75 public static final String SERVER_MSG_ID = "serverMsgId";
76 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
77 public static final String FINGERPRINT = "axolotl_fingerprint";
78 public static final String READ = "read";
79 public static final String ERROR_MESSAGE = "errorMsg";
80 public static final String READ_BY_MARKERS = "readByMarkers";
81 public static final String MARKABLE = "markable";
82 public static final String DELETED = "deleted";
83 public static final String ME_COMMAND = "/me ";
84
85 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
86
87
88 public boolean markable = false;
89 protected String conversationUuid;
90 protected Jid counterpart;
91 protected Jid trueCounterpart;
92 protected String body;
93 protected String encryptedBody;
94 protected long timeSent;
95 protected int encryption;
96 protected int status;
97 protected int type;
98 protected boolean deleted = false;
99 protected boolean carbon = false;
100 protected boolean oob = false;
101 protected List<Edit> edits = new ArrayList<>();
102 protected String relativeFilePath;
103 protected boolean read = true;
104 protected String remoteMsgId = null;
105 private String bodyLanguage = null;
106 protected String serverMsgId = null;
107 private final Conversational conversation;
108 protected Transferable transferable = null;
109 private Message mNextMessage = null;
110 private Message mPreviousMessage = null;
111 private String axolotlFingerprint = null;
112 private String errorMessage = null;
113 private Set<ReadByMarker> readByMarkers = new HashSet<>();
114
115 private Boolean isGeoUri = null;
116 private Boolean isEmojisOnly = null;
117 private Boolean treatAsDownloadable = null;
118 private FileParams fileParams = null;
119 private List<MucOptions.User> counterparts;
120 private WeakReference<MucOptions.User> user;
121
122 protected Message(Conversational conversation) {
123 this.conversation = conversation;
124 }
125
126 public Message(Conversational conversation, String body, int encryption) {
127 this(conversation, body, encryption, STATUS_UNSEND);
128 }
129
130 public Message(Conversational conversation, String body, int encryption, int status) {
131 this(conversation, java.util.UUID.randomUUID().toString(),
132 conversation.getUuid(),
133 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
134 null,
135 body,
136 System.currentTimeMillis(),
137 encryption,
138 status,
139 TYPE_TEXT,
140 false,
141 null,
142 null,
143 null,
144 null,
145 true,
146 null,
147 false,
148 null,
149 null,
150 false,
151 false,
152 null);
153 }
154
155 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
156 this(conversation, java.util.UUID.randomUUID().toString(),
157 conversation.getUuid(),
158 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
159 null,
160 null,
161 System.currentTimeMillis(),
162 Message.ENCRYPTION_NONE,
163 status,
164 type,
165 false,
166 remoteMsgId,
167 null,
168 null,
169 null,
170 true,
171 null,
172 false,
173 null,
174 null,
175 false,
176 false,
177 null);
178 }
179
180 protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
181 final Jid trueCounterpart, final String body, final long timeSent,
182 final int encryption, final int status, final int type, final boolean carbon,
183 final String remoteMsgId, final String relativeFilePath,
184 final String serverMsgId, final String fingerprint, final boolean read,
185 final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
186 final boolean markable, final boolean deleted, final String bodyLanguage) {
187 this.conversation = conversation;
188 this.uuid = uuid;
189 this.conversationUuid = conversationUUid;
190 this.counterpart = counterpart;
191 this.trueCounterpart = trueCounterpart;
192 this.body = body == null ? "" : body;
193 this.timeSent = timeSent;
194 this.encryption = encryption;
195 this.status = status;
196 this.type = type;
197 this.carbon = carbon;
198 this.remoteMsgId = remoteMsgId;
199 this.relativeFilePath = relativeFilePath;
200 this.serverMsgId = serverMsgId;
201 this.axolotlFingerprint = fingerprint;
202 this.read = read;
203 this.edits = Edit.fromJson(edited);
204 this.oob = oob;
205 this.errorMessage = errorMessage;
206 this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
207 this.markable = markable;
208 this.deleted = deleted;
209 this.bodyLanguage = bodyLanguage;
210 }
211
212 public static Message fromCursor(Cursor cursor, Conversation conversation) {
213 return new Message(conversation,
214 cursor.getString(cursor.getColumnIndex(UUID)),
215 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
216 fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
217 fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
218 cursor.getString(cursor.getColumnIndex(BODY)),
219 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
220 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
221 cursor.getInt(cursor.getColumnIndex(STATUS)),
222 cursor.getInt(cursor.getColumnIndex(TYPE)),
223 cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
224 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
225 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
226 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
227 cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
228 cursor.getInt(cursor.getColumnIndex(READ)) > 0,
229 cursor.getString(cursor.getColumnIndex(EDITED)),
230 cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
231 cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
232 ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
233 cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
234 cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
235 cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
236 );
237 }
238
239 private static Jid fromString(String value) {
240 try {
241 if (value != null) {
242 return Jid.of(value);
243 }
244 } catch (IllegalArgumentException e) {
245 return null;
246 }
247 return null;
248 }
249
250 public static Message createStatusMessage(Conversation conversation, String body) {
251 final Message message = new Message(conversation);
252 message.setType(Message.TYPE_STATUS);
253 message.setStatus(Message.STATUS_RECEIVED);
254 message.body = body;
255 return message;
256 }
257
258 public static Message createLoadMoreMessage(Conversation conversation) {
259 final Message message = new Message(conversation);
260 message.setType(Message.TYPE_STATUS);
261 message.body = "LOAD_MORE";
262 return message;
263 }
264
265 @Override
266 public ContentValues getContentValues() {
267 ContentValues values = new ContentValues();
268 values.put(UUID, uuid);
269 values.put(CONVERSATION, conversationUuid);
270 if (counterpart == null) {
271 values.putNull(COUNTERPART);
272 } else {
273 values.put(COUNTERPART, counterpart.toString());
274 }
275 if (trueCounterpart == null) {
276 values.putNull(TRUE_COUNTERPART);
277 } else {
278 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
279 }
280 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
281 values.put(TIME_SENT, timeSent);
282 values.put(ENCRYPTION, encryption);
283 values.put(STATUS, status);
284 values.put(TYPE, type);
285 values.put(CARBON, carbon ? 1 : 0);
286 values.put(REMOTE_MSG_ID, remoteMsgId);
287 values.put(RELATIVE_FILE_PATH, relativeFilePath);
288 values.put(SERVER_MSG_ID, serverMsgId);
289 values.put(FINGERPRINT, axolotlFingerprint);
290 values.put(READ, read ? 1 : 0);
291 try {
292 values.put(EDITED, Edit.toJson(edits));
293 } catch (JSONException e) {
294 Log.e(Config.LOGTAG,"error persisting json for edits",e);
295 }
296 values.put(OOB, oob ? 1 : 0);
297 values.put(ERROR_MESSAGE, errorMessage);
298 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
299 values.put(MARKABLE, markable ? 1 : 0);
300 values.put(DELETED, deleted ? 1 : 0);
301 values.put(BODY_LANGUAGE, bodyLanguage);
302 return values;
303 }
304
305 public String getConversationUuid() {
306 return conversationUuid;
307 }
308
309 public Conversational getConversation() {
310 return this.conversation;
311 }
312
313 public Jid getCounterpart() {
314 return counterpart;
315 }
316
317 public void setCounterpart(final Jid counterpart) {
318 this.counterpart = counterpart;
319 }
320
321 public Contact getContact() {
322 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
323 return this.conversation.getContact();
324 } else {
325 if (this.trueCounterpart == null) {
326 return null;
327 } else {
328 return this.conversation.getAccount().getRoster()
329 .getContactFromContactList(this.trueCounterpart);
330 }
331 }
332 }
333
334 public String getBody() {
335 return body;
336 }
337
338 public synchronized void setBody(String body) {
339 if (body == null) {
340 throw new Error("You should not set the message body to null");
341 }
342 this.body = body;
343 this.isGeoUri = null;
344 this.isEmojisOnly = null;
345 this.treatAsDownloadable = null;
346 this.fileParams = null;
347 }
348
349 public void setMucUser(MucOptions.User user) {
350 this.user = new WeakReference<>(user);
351 }
352
353 public boolean sameMucUser(Message otherMessage) {
354 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
355 final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
356 return thisUser != null && thisUser == otherUser;
357 }
358
359 public String getErrorMessage() {
360 return errorMessage;
361 }
362
363 public boolean setErrorMessage(String message) {
364 boolean changed = (message != null && !message.equals(errorMessage))
365 || (message == null && errorMessage != null);
366 this.errorMessage = message;
367 return changed;
368 }
369
370 public long getTimeSent() {
371 return timeSent;
372 }
373
374 public int getEncryption() {
375 return encryption;
376 }
377
378 public void setEncryption(int encryption) {
379 this.encryption = encryption;
380 }
381
382 public int getStatus() {
383 return status;
384 }
385
386 public void setStatus(int status) {
387 this.status = status;
388 }
389
390 public String getRelativeFilePath() {
391 return this.relativeFilePath;
392 }
393
394 public void setRelativeFilePath(String path) {
395 this.relativeFilePath = path;
396 }
397
398 public String getRemoteMsgId() {
399 return this.remoteMsgId;
400 }
401
402 public void setRemoteMsgId(String id) {
403 this.remoteMsgId = id;
404 }
405
406 public String getServerMsgId() {
407 return this.serverMsgId;
408 }
409
410 public void setServerMsgId(String id) {
411 this.serverMsgId = id;
412 }
413
414 public boolean isRead() {
415 return this.read;
416 }
417
418 public boolean isDeleted() {
419 return this.deleted;
420 }
421
422 public void setDeleted(boolean deleted) {
423 this.deleted = deleted;
424 }
425
426 public void markRead() {
427 this.read = true;
428 }
429
430 public void markUnread() {
431 this.read = false;
432 }
433
434 public void setTime(long time) {
435 this.timeSent = time;
436 }
437
438 public String getEncryptedBody() {
439 return this.encryptedBody;
440 }
441
442 public void setEncryptedBody(String body) {
443 this.encryptedBody = body;
444 }
445
446 public int getType() {
447 return this.type;
448 }
449
450 public void setType(int type) {
451 this.type = type;
452 }
453
454 public boolean isCarbon() {
455 return carbon;
456 }
457
458 public void setCarbon(boolean carbon) {
459 this.carbon = carbon;
460 }
461
462 public void putEdited(String edited, String serverMsgId) {
463 final Edit edit = new Edit(edited, serverMsgId);
464 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
465 this.edits.add(edit);
466 }
467 }
468
469 boolean remoteMsgIdMatchInEdit(String id) {
470 for(Edit edit : this.edits) {
471 if (id.equals(edit.getEditedId())) {
472 return true;
473 }
474 }
475 return false;
476 }
477
478 public String getBodyLanguage() {
479 return this.bodyLanguage;
480 }
481
482 public void setBodyLanguage(String language) {
483 this.bodyLanguage = language;
484 }
485
486 public boolean edited() {
487 return this.edits.size() > 0;
488 }
489
490 public void setTrueCounterpart(Jid trueCounterpart) {
491 this.trueCounterpart = trueCounterpart;
492 }
493
494 public Jid getTrueCounterpart() {
495 return this.trueCounterpart;
496 }
497
498 public Transferable getTransferable() {
499 return this.transferable;
500 }
501
502 public synchronized void setTransferable(Transferable transferable) {
503 this.fileParams = null;
504 this.transferable = transferable;
505 }
506
507 public boolean addReadByMarker(ReadByMarker readByMarker) {
508 if (readByMarker.getRealJid() != null) {
509 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
510 return false;
511 }
512 } else if (readByMarker.getFullJid() != null) {
513 if (readByMarker.getFullJid().equals(counterpart)) {
514 return false;
515 }
516 }
517 if (this.readByMarkers.add(readByMarker)) {
518 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
519 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
520 while (iterator.hasNext()) {
521 ReadByMarker marker = iterator.next();
522 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
523 iterator.remove();
524 }
525 }
526 }
527 return true;
528 } else {
529 return false;
530 }
531 }
532
533 public Set<ReadByMarker> getReadByMarkers() {
534 return Collections.unmodifiableSet(this.readByMarkers);
535 }
536
537 boolean similar(Message message) {
538 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
539 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
540 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
541 return true;
542 } else if (this.body == null || this.counterpart == null) {
543 return false;
544 } else {
545 String body, otherBody;
546 if (this.hasFileOnRemoteHost()) {
547 body = getFileParams().url.toString();
548 otherBody = message.body == null ? null : message.body.trim();
549 } else {
550 body = this.body;
551 otherBody = message.body;
552 }
553 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
554 if (message.getRemoteMsgId() != null) {
555 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
556 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
557 return true;
558 }
559 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
560 && matchingCounterpart
561 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
562 } else {
563 return this.remoteMsgId == null
564 && matchingCounterpart
565 && body.equals(otherBody)
566 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
567 }
568 }
569 }
570
571 public Message next() {
572 if (this.conversation instanceof Conversation) {
573 final Conversation conversation = (Conversation) this.conversation;
574 synchronized (conversation.messages) {
575 if (this.mNextMessage == null) {
576 int index = conversation.messages.indexOf(this);
577 if (index < 0 || index >= conversation.messages.size() - 1) {
578 this.mNextMessage = null;
579 } else {
580 this.mNextMessage = conversation.messages.get(index + 1);
581 }
582 }
583 return this.mNextMessage;
584 }
585 } else {
586 throw new AssertionError("Calling next should be disabled for stubs");
587 }
588 }
589
590 public Message prev() {
591 if (this.conversation instanceof Conversation) {
592 final Conversation conversation = (Conversation) this.conversation;
593 synchronized (conversation.messages) {
594 if (this.mPreviousMessage == null) {
595 int index = conversation.messages.indexOf(this);
596 if (index <= 0 || index > conversation.messages.size()) {
597 this.mPreviousMessage = null;
598 } else {
599 this.mPreviousMessage = conversation.messages.get(index - 1);
600 }
601 }
602 }
603 return this.mPreviousMessage;
604 } else {
605 throw new AssertionError("Calling prev should be disabled for stubs");
606 }
607 }
608
609 public boolean isLastCorrectableMessage() {
610 Message next = next();
611 while (next != null) {
612 if (next.isCorrectable()) {
613 return false;
614 }
615 next = next.next();
616 }
617 return isCorrectable();
618 }
619
620 private boolean isCorrectable() {
621 return getStatus() != STATUS_RECEIVED && !isCarbon();
622 }
623
624 public boolean mergeable(final Message message) {
625 return message != null &&
626 (message.getType() == Message.TYPE_TEXT &&
627 this.getTransferable() == null &&
628 message.getTransferable() == null &&
629 message.getEncryption() != Message.ENCRYPTION_PGP &&
630 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
631 this.getType() == message.getType() &&
632 //this.getStatus() == message.getStatus() &&
633 isStatusMergeable(this.getStatus(), message.getStatus()) &&
634 this.getEncryption() == message.getEncryption() &&
635 this.getCounterpart() != null &&
636 this.getCounterpart().equals(message.getCounterpart()) &&
637 this.edited() == message.edited() &&
638 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
639 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
640 !message.isGeoUri() &&
641 !this.isGeoUri() &&
642 !message.isOOb() &&
643 !this.isOOb() &&
644 !message.treatAsDownloadable() &&
645 !this.treatAsDownloadable() &&
646 !message.hasMeCommand() &&
647 !this.hasMeCommand() &&
648 !this.bodyIsOnlyEmojis() &&
649 !message.bodyIsOnlyEmojis() &&
650 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
651 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
652 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
653 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
654 );
655 }
656
657 private static boolean isStatusMergeable(int a, int b) {
658 return a == b || (
659 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
660 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
661 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
662 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
663 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
664 );
665 }
666
667 public void setCounterparts(List<MucOptions.User> counterparts) {
668 this.counterparts = counterparts;
669 }
670
671 public List<MucOptions.User> getCounterparts() {
672 return this.counterparts;
673 }
674
675 @Override
676 public int getAvatarBackgroundColor() {
677 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
678 return Color.TRANSPARENT;
679 } else {
680 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
681 }
682 }
683
684 public boolean isOOb() {
685 return oob;
686 }
687
688 public static class MergeSeparator {
689 }
690
691 public SpannableStringBuilder getMergedBody() {
692 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
693 Message current = this;
694 while (current.mergeable(current.next())) {
695 current = current.next();
696 if (current == null) {
697 break;
698 }
699 body.append("\n\n");
700 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
701 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
702 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
703 }
704 return body;
705 }
706
707 public boolean hasMeCommand() {
708 return this.body.trim().startsWith(ME_COMMAND);
709 }
710
711 public int getMergedStatus() {
712 int status = this.status;
713 Message current = this;
714 while (current.mergeable(current.next())) {
715 current = current.next();
716 if (current == null) {
717 break;
718 }
719 status = current.status;
720 }
721 return status;
722 }
723
724 public long getMergedTimeSent() {
725 long time = this.timeSent;
726 Message current = this;
727 while (current.mergeable(current.next())) {
728 current = current.next();
729 if (current == null) {
730 break;
731 }
732 time = current.timeSent;
733 }
734 return time;
735 }
736
737 public boolean wasMergedIntoPrevious() {
738 Message prev = this.prev();
739 return prev != null && prev.mergeable(this);
740 }
741
742 public boolean trusted() {
743 Contact contact = this.getContact();
744 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
745 }
746
747 public boolean fixCounterpart() {
748 Presences presences = conversation.getContact().getPresences();
749 if (counterpart != null && presences.has(counterpart.getResource())) {
750 return true;
751 } else if (presences.size() >= 1) {
752 try {
753 counterpart = Jid.of(conversation.getJid().getLocal(),
754 conversation.getJid().getDomain(),
755 presences.toResourceArray()[0]);
756 return true;
757 } catch (IllegalArgumentException e) {
758 counterpart = null;
759 return false;
760 }
761 } else {
762 counterpart = null;
763 return false;
764 }
765 }
766
767 public void setUuid(String uuid) {
768 this.uuid = uuid;
769 }
770
771 public String getEditedId() {
772 if (edits.size() > 0) {
773 return edits.get(edits.size() - 1).getEditedId();
774 } else {
775 throw new IllegalStateException("Attempting to store unedited message");
776 }
777 }
778
779 public String getEditedIdWireFormat() {
780 if (edits.size() > 0) {
781 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
782 } else {
783 throw new IllegalStateException("Attempting to store unedited message");
784 }
785 }
786
787 public void setOob(boolean isOob) {
788 this.oob = isOob;
789 }
790
791 public String getMimeType() {
792 String extension;
793 if (relativeFilePath != null) {
794 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
795 } else {
796 try {
797 final URL url = new URL(body.split("\n")[0]);
798 extension = MimeUtils.extractRelevantExtension(url);
799 } catch (MalformedURLException e) {
800 return null;
801 }
802 }
803 return MimeUtils.guessMimeTypeFromExtension(extension);
804 }
805
806 public synchronized boolean treatAsDownloadable() {
807 if (treatAsDownloadable == null) {
808 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
809 }
810 return treatAsDownloadable;
811 }
812
813 public synchronized boolean bodyIsOnlyEmojis() {
814 if (isEmojisOnly == null) {
815 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
816 }
817 return isEmojisOnly;
818 }
819
820 public synchronized boolean isGeoUri() {
821 if (isGeoUri == null) {
822 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
823 }
824 return isGeoUri;
825 }
826
827 public synchronized void resetFileParams() {
828 this.fileParams = null;
829 }
830
831 public synchronized FileParams getFileParams() {
832 if (fileParams == null) {
833 fileParams = new FileParams();
834 if (this.transferable != null) {
835 fileParams.size = this.transferable.getFileSize();
836 }
837 final String[] parts = body == null ? new String[0] : body.split("\\|");
838 switch (parts.length) {
839 case 1:
840 try {
841 fileParams.size = Long.parseLong(parts[0]);
842 } catch (NumberFormatException e) {
843 fileParams.url = parseUrl(parts[0]);
844 }
845 break;
846 case 5:
847 fileParams.runtime = parseInt(parts[4]);
848 case 4:
849 fileParams.width = parseInt(parts[2]);
850 fileParams.height = parseInt(parts[3]);
851 case 2:
852 fileParams.url = parseUrl(parts[0]);
853 fileParams.size = parseLong(parts[1]);
854 break;
855 case 3:
856 fileParams.size = parseLong(parts[0]);
857 fileParams.width = parseInt(parts[1]);
858 fileParams.height = parseInt(parts[2]);
859 break;
860 }
861 }
862 return fileParams;
863 }
864
865 private static long parseLong(String value) {
866 try {
867 return Long.parseLong(value);
868 } catch (NumberFormatException e) {
869 return 0;
870 }
871 }
872
873 private static int parseInt(String value) {
874 try {
875 return Integer.parseInt(value);
876 } catch (NumberFormatException e) {
877 return 0;
878 }
879 }
880
881 private static URL parseUrl(String value) {
882 try {
883 return new URL(value);
884 } catch (MalformedURLException e) {
885 return null;
886 }
887 }
888
889 public void untie() {
890 this.mNextMessage = null;
891 this.mPreviousMessage = null;
892 }
893
894 public boolean isPrivateMessage() {
895 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
896 }
897
898 public boolean isFileOrImage() {
899 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
900 }
901
902 public boolean hasFileOnRemoteHost() {
903 return isFileOrImage() && getFileParams().url != null;
904 }
905
906 public boolean needsUploading() {
907 return isFileOrImage() && getFileParams().url == null;
908 }
909
910 public class FileParams {
911 public URL url;
912 public long size = 0;
913 public int width = 0;
914 public int height = 0;
915 public int runtime = 0;
916 }
917
918 public void setFingerprint(String fingerprint) {
919 this.axolotlFingerprint = fingerprint;
920 }
921
922 public String getFingerprint() {
923 return axolotlFingerprint;
924 }
925
926 public boolean isTrusted() {
927 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
928 return s != null && s.isTrusted();
929 }
930
931 private int getPreviousEncryption() {
932 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
933 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
934 continue;
935 }
936 return iterator.getEncryption();
937 }
938 return ENCRYPTION_NONE;
939 }
940
941 private int getNextEncryption() {
942 if (this.conversation instanceof Conversation) {
943 Conversation conversation = (Conversation) this.conversation;
944 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
945 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
946 continue;
947 }
948 return iterator.getEncryption();
949 }
950 return conversation.getNextEncryption();
951 } else {
952 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
953 }
954 }
955
956 public boolean isValidInSession() {
957 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
958 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
959
960 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
961 || futureEncryption == ENCRYPTION_NONE
962 || pastEncryption != futureEncryption;
963
964 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
965 }
966
967 private static int getCleanedEncryption(int encryption) {
968 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
969 return ENCRYPTION_PGP;
970 }
971 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
972 return ENCRYPTION_AXOLOTL;
973 }
974 return encryption;
975 }
976
977 public static boolean configurePrivateMessage(final Message message) {
978 return configurePrivateMessage(message, false);
979 }
980
981 public static boolean configurePrivateFileMessage(final Message message) {
982 return configurePrivateMessage(message, true);
983 }
984
985 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
986 final Conversation conversation;
987 if (message.conversation instanceof Conversation) {
988 conversation = (Conversation) message.conversation;
989 } else {
990 return false;
991 }
992 if (conversation.getMode() == Conversation.MODE_MULTI) {
993 final Jid nextCounterpart = conversation.getNextCounterpart();
994 if (nextCounterpart != null) {
995 message.setCounterpart(nextCounterpart);
996 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
997 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
998 return true;
999 }
1000 }
1001 return false;
1002 }
1003}