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