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