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