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