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.net.MalformedURLException;
16import java.net.URL;
17import java.util.ArrayList;
18import java.util.HashSet;
19import java.util.Iterator;
20import java.util.List;
21import java.util.Set;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
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 HashSet<>();
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 HashSet<>() : 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.toString();
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 try {
798 final URL url = new URL(body.split("\n")[0]);
799 extension = MimeUtils.extractRelevantExtension(url);
800 } catch (MalformedURLException e) {
801 return null;
802 }
803 }
804 return MimeUtils.guessMimeTypeFromExtension(extension);
805 }
806
807 public synchronized boolean treatAsDownloadable() {
808 if (treatAsDownloadable == null) {
809 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
810 }
811 return treatAsDownloadable;
812 }
813
814 public synchronized boolean bodyIsOnlyEmojis() {
815 if (isEmojisOnly == null) {
816 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
817 }
818 return isEmojisOnly;
819 }
820
821 public synchronized boolean isGeoUri() {
822 if (isGeoUri == null) {
823 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
824 }
825 return isGeoUri;
826 }
827
828 public synchronized void resetFileParams() {
829 this.fileParams = null;
830 }
831
832 public synchronized FileParams getFileParams() {
833 if (fileParams == null) {
834 fileParams = new FileParams();
835 if (this.transferable != null) {
836 fileParams.size = this.transferable.getFileSize();
837 }
838 final String[] parts = body == null ? new String[0] : body.split("\\|");
839 switch (parts.length) {
840 case 1:
841 try {
842 fileParams.size = Long.parseLong(parts[0]);
843 } catch (NumberFormatException e) {
844 fileParams.url = parseUrl(parts[0]);
845 }
846 break;
847 case 5:
848 fileParams.runtime = parseInt(parts[4]);
849 case 4:
850 fileParams.width = parseInt(parts[2]);
851 fileParams.height = parseInt(parts[3]);
852 case 2:
853 fileParams.url = parseUrl(parts[0]);
854 fileParams.size = parseLong(parts[1]);
855 break;
856 case 3:
857 fileParams.size = parseLong(parts[0]);
858 fileParams.width = parseInt(parts[1]);
859 fileParams.height = parseInt(parts[2]);
860 break;
861 }
862 }
863 return fileParams;
864 }
865
866 private static long parseLong(String value) {
867 try {
868 return Long.parseLong(value);
869 } catch (NumberFormatException e) {
870 return 0;
871 }
872 }
873
874 private static int parseInt(String value) {
875 try {
876 return Integer.parseInt(value);
877 } catch (NumberFormatException e) {
878 return 0;
879 }
880 }
881
882 private static URL parseUrl(String value) {
883 try {
884 return new URL(value);
885 } catch (MalformedURLException e) {
886 return null;
887 }
888 }
889
890 public void untie() {
891 this.mNextMessage = null;
892 this.mPreviousMessage = null;
893 }
894
895 public boolean isPrivateMessage() {
896 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
897 }
898
899 public boolean isFileOrImage() {
900 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
901 }
902
903 public boolean hasFileOnRemoteHost() {
904 return isFileOrImage() && getFileParams().url != null;
905 }
906
907 public boolean needsUploading() {
908 return isFileOrImage() && getFileParams().url == null;
909 }
910
911 public class FileParams {
912 public URL url;
913 public long size = 0;
914 public int width = 0;
915 public int height = 0;
916 public int runtime = 0;
917 }
918
919 public void setFingerprint(String fingerprint) {
920 this.axolotlFingerprint = fingerprint;
921 }
922
923 public String getFingerprint() {
924 return axolotlFingerprint;
925 }
926
927 public boolean isTrusted() {
928 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
929 return s != null && s.isTrusted();
930 }
931
932 private int getPreviousEncryption() {
933 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
934 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
935 continue;
936 }
937 return iterator.getEncryption();
938 }
939 return ENCRYPTION_NONE;
940 }
941
942 private int getNextEncryption() {
943 if (this.conversation instanceof Conversation) {
944 Conversation conversation = (Conversation) this.conversation;
945 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
946 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
947 continue;
948 }
949 return iterator.getEncryption();
950 }
951 return conversation.getNextEncryption();
952 } else {
953 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
954 }
955 }
956
957 public boolean isValidInSession() {
958 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
959 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
960
961 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
962 || futureEncryption == ENCRYPTION_NONE
963 || pastEncryption != futureEncryption;
964
965 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
966 }
967
968 private static int getCleanedEncryption(int encryption) {
969 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
970 return ENCRYPTION_PGP;
971 }
972 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
973 return ENCRYPTION_AXOLOTL;
974 }
975 return encryption;
976 }
977
978 public static boolean configurePrivateMessage(final Message message) {
979 return configurePrivateMessage(message, false);
980 }
981
982 public static boolean configurePrivateFileMessage(final Message message) {
983 return configurePrivateMessage(message, true);
984 }
985
986 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
987 final Conversation conversation;
988 if (message.conversation instanceof Conversation) {
989 conversation = (Conversation) message.conversation;
990 } else {
991 return false;
992 }
993 if (conversation.getMode() == Conversation.MODE_MULTI) {
994 final Jid nextCounterpart = conversation.getNextCounterpart();
995 if (nextCounterpart != null) {
996 message.setCounterpart(nextCounterpart);
997 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
998 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
999 return true;
1000 }
1001 }
1002 return false;
1003 }
1004}