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.Collection;
20import java.util.Collections;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Set;
24import java.util.concurrent.CopyOnWriteArraySet;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.crypto.axolotl.AxolotlService;
28import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
29import eu.siacs.conversations.http.URL;
30import eu.siacs.conversations.services.AvatarService;
31import eu.siacs.conversations.ui.util.PresenceSelector;
32import eu.siacs.conversations.utils.CryptoHelper;
33import eu.siacs.conversations.utils.Emoticons;
34import eu.siacs.conversations.utils.GeoHelper;
35import eu.siacs.conversations.utils.MessageUtils;
36import eu.siacs.conversations.utils.MimeUtils;
37import eu.siacs.conversations.utils.UIHelper;
38import eu.siacs.conversations.xmpp.Jid;
39
40public class Message extends AbstractEntity implements AvatarService.Avatarable {
41
42 public static final String TABLENAME = "messages";
43
44 public static final int STATUS_RECEIVED = 0;
45 public static final int STATUS_UNSEND = 1;
46 public static final int STATUS_SEND = 2;
47 public static final int STATUS_SEND_FAILED = 3;
48 public static final int STATUS_WAITING = 5;
49 public static final int STATUS_OFFERED = 6;
50 public static final int STATUS_SEND_RECEIVED = 7;
51 public static final int STATUS_SEND_DISPLAYED = 8;
52
53 public static final int ENCRYPTION_NONE = 0;
54 public static final int ENCRYPTION_PGP = 1;
55 public static final int ENCRYPTION_OTR = 2;
56 public static final int ENCRYPTION_DECRYPTED = 3;
57 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
58 public static final int ENCRYPTION_AXOLOTL = 5;
59 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
60 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
61
62 public static final int TYPE_TEXT = 0;
63 public static final int TYPE_IMAGE = 1;
64 public static final int TYPE_FILE = 2;
65 public static final int TYPE_STATUS = 3;
66 public static final int TYPE_PRIVATE = 4;
67 public static final int TYPE_PRIVATE_FILE = 5;
68 public static final int TYPE_RTP_SESSION = 6;
69
70 public static final String CONVERSATION = "conversationUuid";
71 public static final String COUNTERPART = "counterpart";
72 public static final String TRUE_COUNTERPART = "trueCounterpart";
73 public static final String BODY = "body";
74 public static final String BODY_LANGUAGE = "bodyLanguage";
75 public static final String TIME_SENT = "timeSent";
76 public static final String ENCRYPTION = "encryption";
77 public static final String STATUS = "status";
78 public static final String TYPE = "type";
79 public static final String CARBON = "carbon";
80 public static final String OOB = "oob";
81 public static final String EDITED = "edited";
82 public static final String REMOTE_MSG_ID = "remoteMsgId";
83 public static final String SERVER_MSG_ID = "serverMsgId";
84 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
85 public static final String FINGERPRINT = "axolotl_fingerprint";
86 public static final String READ = "read";
87 public static final String ERROR_MESSAGE = "errorMsg";
88 public static final String READ_BY_MARKERS = "readByMarkers";
89 public static final String MARKABLE = "markable";
90 public static final String DELETED = "deleted";
91 public static final String OCCUPANT_ID = "occupantId";
92 public static final String REACTIONS = "reactions";
93 public static final String ME_COMMAND = "/me ";
94
95 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
96
97
98 public boolean markable = false;
99 protected String conversationUuid;
100 protected Jid counterpart;
101 protected Jid trueCounterpart;
102 protected String body;
103 protected String encryptedBody;
104 protected long timeSent;
105 protected int encryption;
106 protected int status;
107 protected int type;
108 protected boolean deleted = false;
109 protected boolean carbon = false;
110 protected boolean oob = false;
111 protected List<Edit> edits = new ArrayList<>();
112 protected String relativeFilePath;
113 protected boolean read = true;
114 protected String remoteMsgId = null;
115 private String bodyLanguage = null;
116 protected String serverMsgId = null;
117 private final Conversational conversation;
118 protected Transferable transferable = null;
119 private Message mNextMessage = null;
120 private Message mPreviousMessage = null;
121 private String axolotlFingerprint = null;
122 private String errorMessage = null;
123 private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
124 private String occupantId;
125 private Collection<Reaction> reactions = Collections.emptyList();
126
127 private Boolean isGeoUri = null;
128 private Boolean isEmojisOnly = null;
129 private Boolean treatAsDownloadable = null;
130 private FileParams fileParams = null;
131 private List<MucOptions.User> counterparts;
132 private WeakReference<MucOptions.User> user;
133
134 protected Message(Conversational conversation) {
135 this.conversation = conversation;
136 }
137
138 public Message(Conversational conversation, String body, int encryption) {
139 this(conversation, body, encryption, STATUS_UNSEND);
140 }
141
142 public Message(Conversational conversation, String body, int encryption, int status) {
143 this(conversation, java.util.UUID.randomUUID().toString(),
144 conversation.getUuid(),
145 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
146 null,
147 body,
148 System.currentTimeMillis(),
149 encryption,
150 status,
151 TYPE_TEXT,
152 false,
153 null,
154 null,
155 null,
156 null,
157 true,
158 null,
159 false,
160 null,
161 null,
162 false,
163 false,
164 null,
165 null,
166 Collections.emptyList());
167 }
168
169 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
170 this(conversation, java.util.UUID.randomUUID().toString(),
171 conversation.getUuid(),
172 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
173 null,
174 null,
175 System.currentTimeMillis(),
176 Message.ENCRYPTION_NONE,
177 status,
178 type,
179 false,
180 remoteMsgId,
181 null,
182 null,
183 null,
184 true,
185 null,
186 false,
187 null,
188 null,
189 false,
190 false,
191 null,
192 null,
193 Collections.emptyList());
194 }
195
196 protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
197 final Jid trueCounterpart, final String body, final long timeSent,
198 final int encryption, final int status, final int type, final boolean carbon,
199 final String remoteMsgId, final String relativeFilePath,
200 final String serverMsgId, final String fingerprint, final boolean read,
201 final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
202 final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection<Reaction> reactions) {
203 this.conversation = conversation;
204 this.uuid = uuid;
205 this.conversationUuid = conversationUUid;
206 this.counterpart = counterpart;
207 this.trueCounterpart = trueCounterpart;
208 this.body = body == null ? "" : body;
209 this.timeSent = timeSent;
210 this.encryption = encryption;
211 this.status = status;
212 this.type = type;
213 this.carbon = carbon;
214 this.remoteMsgId = remoteMsgId;
215 this.relativeFilePath = relativeFilePath;
216 this.serverMsgId = serverMsgId;
217 this.axolotlFingerprint = fingerprint;
218 this.read = read;
219 this.edits = Edit.fromJson(edited);
220 this.oob = oob;
221 this.errorMessage = errorMessage;
222 this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
223 this.markable = markable;
224 this.deleted = deleted;
225 this.bodyLanguage = bodyLanguage;
226 this.occupantId = occupantId;
227 this.reactions = reactions;
228 }
229
230 public static Message fromCursor(Cursor cursor, Conversation conversation) {
231 return new Message(conversation,
232 cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
233 cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)),
234 fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))),
235 fromString(cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART))),
236 cursor.getString(cursor.getColumnIndexOrThrow(BODY)),
237 cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)),
238 cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)),
239 cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
240 cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
241 cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0,
242 cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)),
243 cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)),
244 cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)),
245 cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)),
246 cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0,
247 cursor.getString(cursor.getColumnIndexOrThrow(EDITED)),
248 cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0,
249 cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)),
250 ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))),
251 cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0,
252 cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0,
253 cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)),
254 cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)),
255 Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS)))
256
257 );
258 }
259
260 private static Jid fromString(String value) {
261 try {
262 if (value != null) {
263 return Jid.of(value);
264 }
265 } catch (IllegalArgumentException e) {
266 return null;
267 }
268 return null;
269 }
270
271 public static Message createStatusMessage(Conversation conversation, String body) {
272 final Message message = new Message(conversation);
273 message.setType(Message.TYPE_STATUS);
274 message.setStatus(Message.STATUS_RECEIVED);
275 message.body = body;
276 return message;
277 }
278
279 public static Message createLoadMoreMessage(Conversation conversation) {
280 final Message message = new Message(conversation);
281 message.setType(Message.TYPE_STATUS);
282 message.body = "LOAD_MORE";
283 return message;
284 }
285
286 @Override
287 public ContentValues getContentValues() {
288 final var values = new ContentValues();
289 values.put(UUID, uuid);
290 values.put(CONVERSATION, conversationUuid);
291 if (counterpart == null) {
292 values.putNull(COUNTERPART);
293 } else {
294 values.put(COUNTERPART, counterpart.toString());
295 }
296 if (trueCounterpart == null) {
297 values.putNull(TRUE_COUNTERPART);
298 } else {
299 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
300 }
301 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
302 values.put(TIME_SENT, timeSent);
303 values.put(ENCRYPTION, encryption);
304 values.put(STATUS, status);
305 values.put(TYPE, type);
306 values.put(CARBON, carbon ? 1 : 0);
307 values.put(REMOTE_MSG_ID, remoteMsgId);
308 values.put(RELATIVE_FILE_PATH, relativeFilePath);
309 values.put(SERVER_MSG_ID, serverMsgId);
310 values.put(FINGERPRINT, axolotlFingerprint);
311 values.put(READ, read ? 1 : 0);
312 try {
313 values.put(EDITED, Edit.toJson(edits));
314 } catch (JSONException e) {
315 Log.e(Config.LOGTAG, "error persisting json for edits", e);
316 }
317 values.put(OOB, oob ? 1 : 0);
318 values.put(ERROR_MESSAGE, errorMessage);
319 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
320 values.put(MARKABLE, markable ? 1 : 0);
321 values.put(DELETED, deleted ? 1 : 0);
322 values.put(BODY_LANGUAGE, bodyLanguage);
323 values.put(OCCUPANT_ID, occupantId);
324 values.put(REACTIONS, Reaction.toString(this.reactions));
325 return values;
326 }
327
328 public String getConversationUuid() {
329 return conversationUuid;
330 }
331
332 public Conversational getConversation() {
333 return this.conversation;
334 }
335
336 public Jid getCounterpart() {
337 return counterpart;
338 }
339
340 public void setCounterpart(final Jid counterpart) {
341 this.counterpart = counterpart;
342 }
343
344 public Contact getContact() {
345 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
346 return this.conversation.getContact();
347 } else {
348 if (this.trueCounterpart == null) {
349 return null;
350 } else {
351 return this.conversation.getAccount().getRoster()
352 .getContactFromContactList(this.trueCounterpart);
353 }
354 }
355 }
356
357 public String getBody() {
358 return body;
359 }
360
361 public synchronized void setBody(String body) {
362 if (body == null) {
363 throw new Error("You should not set the message body to null");
364 }
365 this.body = body;
366 this.isGeoUri = null;
367 this.isEmojisOnly = null;
368 this.treatAsDownloadable = null;
369 this.fileParams = null;
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(final 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 public Set<Jid> getReadyByTrue() {
561 return ImmutableSet.copyOf(
562 Collections2.transform(
563 Collections2.filter(this.readByMarkers, m -> m.getRealJid() != null),
564 ReadByMarker::getRealJid));
565 }
566
567 boolean similar(Message message) {
568 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
569 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
570 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
571 return true;
572 } else if (this.body == null || this.counterpart == null) {
573 return false;
574 } else {
575 String body, otherBody;
576 if (this.hasFileOnRemoteHost()) {
577 body = getFileParams().url;
578 otherBody = message.body == null ? null : message.body.trim();
579 } else {
580 body = this.body;
581 otherBody = message.body;
582 }
583 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
584 if (message.getRemoteMsgId() != null) {
585 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
586 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
587 return true;
588 }
589 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
590 && matchingCounterpart
591 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
592 } else {
593 return this.remoteMsgId == null
594 && matchingCounterpart
595 && body.equals(otherBody)
596 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
597 }
598 }
599 }
600
601 public Message next() {
602 if (this.conversation instanceof Conversation) {
603 final Conversation conversation = (Conversation) this.conversation;
604 synchronized (conversation.messages) {
605 if (this.mNextMessage == null) {
606 int index = conversation.messages.indexOf(this);
607 if (index < 0 || index >= conversation.messages.size() - 1) {
608 this.mNextMessage = null;
609 } else {
610 this.mNextMessage = conversation.messages.get(index + 1);
611 }
612 }
613 return this.mNextMessage;
614 }
615 } else {
616 throw new AssertionError("Calling next should be disabled for stubs");
617 }
618 }
619
620 public Message prev() {
621 if (this.conversation instanceof Conversation) {
622 final Conversation conversation = (Conversation) this.conversation;
623 synchronized (conversation.messages) {
624 if (this.mPreviousMessage == null) {
625 int index = conversation.messages.indexOf(this);
626 if (index <= 0 || index > conversation.messages.size()) {
627 this.mPreviousMessage = null;
628 } else {
629 this.mPreviousMessage = conversation.messages.get(index - 1);
630 }
631 }
632 }
633 return this.mPreviousMessage;
634 } else {
635 throw new AssertionError("Calling prev should be disabled for stubs");
636 }
637 }
638
639 public boolean isLastCorrectableMessage() {
640 Message next = next();
641 while (next != null) {
642 if (next.isEditable()) {
643 return false;
644 }
645 next = next.next();
646 }
647 return isEditable();
648 }
649
650 public boolean isEditable() {
651 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
652 }
653
654 public boolean mergeable(final Message message) {
655 return message != null &&
656 (message.getType() == Message.TYPE_TEXT &&
657 this.getTransferable() == null &&
658 message.getTransferable() == null &&
659 message.getEncryption() != Message.ENCRYPTION_PGP &&
660 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
661 this.getType() == message.getType() &&
662 this.isReactionsEmpty() &&
663 message.isReactionsEmpty() &&
664 isStatusMergeable(this.getStatus(), message.getStatus()) &&
665 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
666 this.getCounterpart() != null &&
667 this.getCounterpart().equals(message.getCounterpart()) &&
668 this.edited() == message.edited() &&
669 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
670 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
671 !message.isGeoUri() &&
672 !this.isGeoUri() &&
673 !message.isOOb() &&
674 !this.isOOb() &&
675 !message.treatAsDownloadable() &&
676 !this.treatAsDownloadable() &&
677 !message.hasMeCommand() &&
678 !this.hasMeCommand() &&
679 !this.bodyIsOnlyEmojis() &&
680 !message.bodyIsOnlyEmojis() &&
681 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
682 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
683 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
684 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
685 );
686 }
687
688 private static boolean isStatusMergeable(int a, int b) {
689 return a == b || (
690 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
691 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
692 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
693 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
694 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
695 );
696 }
697
698 private static boolean isEncryptionMergeable(final int a, final int b) {
699 return a == b
700 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
701 .contains(a);
702 }
703
704 public void setCounterparts(List<MucOptions.User> counterparts) {
705 this.counterparts = counterparts;
706 }
707
708 public List<MucOptions.User> getCounterparts() {
709 return this.counterparts;
710 }
711
712 @Override
713 public int getAvatarBackgroundColor() {
714 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
715 return Color.TRANSPARENT;
716 } else {
717 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
718 }
719 }
720
721 @Override
722 public String getAvatarName() {
723 return UIHelper.getMessageDisplayName(this);
724 }
725
726 public boolean isOOb() {
727 return oob;
728 }
729
730 public void setOccupantId(final String id) {
731 this.occupantId = id;
732 }
733
734 public String getOccupantId() {
735 return this.occupantId;
736 }
737
738 public Collection<Reaction> getReactions() {
739 return this.reactions;
740 }
741
742 public boolean isReactionsEmpty() {
743 return this.reactions.isEmpty();
744 }
745
746 public Reaction.Aggregated getAggregatedReactions() {
747 return Reaction.aggregated(this.reactions);
748 }
749
750 public void setReactions(final Collection<Reaction> reactions) {
751 this.reactions = reactions;
752 }
753
754 public static class MergeSeparator {
755 }
756
757 public SpannableStringBuilder getMergedBody() {
758 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
759 Message current = this;
760 while (current.mergeable(current.next())) {
761 current = current.next();
762 if (current == null) {
763 break;
764 }
765 body.append("\n\n");
766 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
767 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
768 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
769 }
770 return body;
771 }
772
773 public boolean hasMeCommand() {
774 return this.body.trim().startsWith(ME_COMMAND);
775 }
776
777 public int getMergedStatus() {
778 int status = this.status;
779 Message current = this;
780 while (current.mergeable(current.next())) {
781 current = current.next();
782 if (current == null) {
783 break;
784 }
785 status = current.status;
786 }
787 return status;
788 }
789
790 public long getMergedTimeSent() {
791 long time = this.timeSent;
792 Message current = this;
793 while (current.mergeable(current.next())) {
794 current = current.next();
795 if (current == null) {
796 break;
797 }
798 time = current.timeSent;
799 }
800 return time;
801 }
802
803 public boolean wasMergedIntoPrevious() {
804 Message prev = this.prev();
805 return prev != null && prev.mergeable(this);
806 }
807
808 public boolean trusted() {
809 Contact contact = this.getContact();
810 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
811 }
812
813 public boolean fixCounterpart() {
814 final Presences presences = conversation.getContact().getPresences();
815 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
816 return true;
817 } else if (presences.size() >= 1) {
818 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
819 return true;
820 } else {
821 counterpart = null;
822 return false;
823 }
824 }
825
826 public void setUuid(String uuid) {
827 this.uuid = uuid;
828 }
829
830 public String getEditedId() {
831 if (edits.size() > 0) {
832 return edits.get(edits.size() - 1).getEditedId();
833 } else {
834 throw new IllegalStateException("Attempting to store unedited message");
835 }
836 }
837
838 public String getEditedIdWireFormat() {
839 if (edits.size() > 0) {
840 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
841 } else {
842 throw new IllegalStateException("Attempting to store unedited message");
843 }
844 }
845
846 public void setOob(boolean isOob) {
847 this.oob = isOob;
848 }
849
850 public String getMimeType() {
851 String extension;
852 if (relativeFilePath != null) {
853 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
854 } else {
855 final String url = URL.tryParse(body.split("\n")[0]);
856 if (url == null) {
857 return null;
858 }
859 extension = MimeUtils.extractRelevantExtension(url);
860 }
861 return MimeUtils.guessMimeTypeFromExtension(extension);
862 }
863
864 public synchronized boolean treatAsDownloadable() {
865 if (treatAsDownloadable == null) {
866 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
867 }
868 return treatAsDownloadable;
869 }
870
871 public synchronized boolean bodyIsOnlyEmojis() {
872 if (isEmojisOnly == null) {
873 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
874 }
875 return isEmojisOnly;
876 }
877
878 public synchronized boolean isGeoUri() {
879 if (isGeoUri == null) {
880 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
881 }
882 return isGeoUri;
883 }
884
885 public synchronized void resetFileParams() {
886 this.fileParams = null;
887 }
888
889 public synchronized FileParams getFileParams() {
890 if (fileParams == null) {
891 fileParams = new FileParams();
892 if (this.transferable != null) {
893 fileParams.size = this.transferable.getFileSize();
894 }
895 final String[] parts = body == null ? new String[0] : body.split("\\|");
896 switch (parts.length) {
897 case 1:
898 try {
899 fileParams.size = Long.parseLong(parts[0]);
900 } catch (final NumberFormatException e) {
901 fileParams.url = URL.tryParse(parts[0]);
902 }
903 break;
904 case 5:
905 fileParams.runtime = parseInt(parts[4]);
906 case 4:
907 fileParams.width = parseInt(parts[2]);
908 fileParams.height = parseInt(parts[3]);
909 case 2:
910 fileParams.url = URL.tryParse(parts[0]);
911 fileParams.size = Longs.tryParse(parts[1]);
912 break;
913 case 3:
914 fileParams.size = Longs.tryParse(parts[0]);
915 fileParams.width = parseInt(parts[1]);
916 fileParams.height = parseInt(parts[2]);
917 break;
918 }
919 }
920 return fileParams;
921 }
922
923 private static int parseInt(String value) {
924 try {
925 return Integer.parseInt(value);
926 } catch (NumberFormatException e) {
927 return 0;
928 }
929 }
930
931 public void untie() {
932 this.mNextMessage = null;
933 this.mPreviousMessage = null;
934 }
935
936 public boolean isPrivateMessage() {
937 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
938 }
939
940 public boolean isFileOrImage() {
941 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
942 }
943
944
945 public boolean isTypeText() {
946 return type == TYPE_TEXT || type == TYPE_PRIVATE;
947 }
948
949 public boolean hasFileOnRemoteHost() {
950 return isFileOrImage() && getFileParams().url != null;
951 }
952
953 public boolean needsUploading() {
954 return isFileOrImage() && getFileParams().url == null;
955 }
956
957 public static class FileParams {
958 public String url;
959 public Long size = null;
960 public int width = 0;
961 public int height = 0;
962 public int runtime = 0;
963
964 public long getSize() {
965 return size == null ? 0 : size;
966 }
967 }
968
969 public void setFingerprint(String fingerprint) {
970 this.axolotlFingerprint = fingerprint;
971 }
972
973 public String getFingerprint() {
974 return axolotlFingerprint;
975 }
976
977 public boolean isTrusted() {
978 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
979 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
980 return s != null && s.isTrusted();
981 }
982
983 private int getPreviousEncryption() {
984 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
985 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
986 continue;
987 }
988 return iterator.getEncryption();
989 }
990 return ENCRYPTION_NONE;
991 }
992
993 private int getNextEncryption() {
994 if (this.conversation instanceof Conversation) {
995 Conversation conversation = (Conversation) this.conversation;
996 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
997 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
998 continue;
999 }
1000 return iterator.getEncryption();
1001 }
1002 return conversation.getNextEncryption();
1003 } else {
1004 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1005 }
1006 }
1007
1008 public boolean isValidInSession() {
1009 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1010 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1011
1012 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1013 || futureEncryption == ENCRYPTION_NONE
1014 || pastEncryption != futureEncryption;
1015
1016 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1017 }
1018
1019 private static int getCleanedEncryption(int encryption) {
1020 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1021 return ENCRYPTION_PGP;
1022 }
1023 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1024 return ENCRYPTION_AXOLOTL;
1025 }
1026 return encryption;
1027 }
1028
1029 public static boolean configurePrivateMessage(final Message message) {
1030 return configurePrivateMessage(message, false);
1031 }
1032
1033 public static boolean configurePrivateFileMessage(final Message message) {
1034 return configurePrivateMessage(message, true);
1035 }
1036
1037 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1038 final Conversation conversation;
1039 if (message.conversation instanceof Conversation) {
1040 conversation = (Conversation) message.conversation;
1041 } else {
1042 return false;
1043 }
1044 if (conversation.getMode() == Conversation.MODE_MULTI) {
1045 final Jid nextCounterpart = conversation.getNextCounterpart();
1046 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1047 }
1048 return false;
1049 }
1050
1051 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1052 final Conversation conversation;
1053 if (message.conversation instanceof Conversation) {
1054 conversation = (Conversation) message.conversation;
1055 } else {
1056 return false;
1057 }
1058 return configurePrivateMessage(conversation, message, counterpart, false);
1059 }
1060
1061 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1062 if (counterpart == null) {
1063 return false;
1064 }
1065 message.setCounterpart(counterpart);
1066 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1067 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1068 return true;
1069 }
1070}