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