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