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.treatAsDownloadable() &&
617 !this.treatAsDownloadable() &&
618 !message.getBody().startsWith(ME_COMMAND) &&
619 !this.getBody().startsWith(ME_COMMAND) &&
620 !this.bodyIsOnlyEmojis() &&
621 !message.bodyIsOnlyEmojis() &&
622 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
623 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
624 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
625 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
626 );
627 }
628
629 private static boolean isStatusMergeable(int a, int b) {
630 return a == b || (
631 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
632 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
633 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
634 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
635 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
636 );
637 }
638
639 public void setCounterparts(List<MucOptions.User> counterparts) {
640 this.counterparts = counterparts;
641 }
642
643 public List<MucOptions.User> getCounterparts() {
644 return this.counterparts;
645 }
646
647 @Override
648 public int getAvatarBackgroundColor() {
649 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
650 return Color.TRANSPARENT;
651 } else {
652 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
653 }
654 }
655
656 public boolean isOOb() {
657 return oob;
658 }
659
660 public static class MergeSeparator {
661 }
662
663 public SpannableStringBuilder getMergedBody() {
664 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
665 Message current = this;
666 while (current.mergeable(current.next())) {
667 current = current.next();
668 if (current == null) {
669 break;
670 }
671 body.append("\n\n");
672 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
673 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
674 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
675 }
676 return body;
677 }
678
679 public boolean hasMeCommand() {
680 return this.body.trim().startsWith(ME_COMMAND);
681 }
682
683 public int getMergedStatus() {
684 int status = this.status;
685 Message current = this;
686 while (current.mergeable(current.next())) {
687 current = current.next();
688 if (current == null) {
689 break;
690 }
691 status = current.status;
692 }
693 return status;
694 }
695
696 public long getMergedTimeSent() {
697 long time = this.timeSent;
698 Message current = this;
699 while (current.mergeable(current.next())) {
700 current = current.next();
701 if (current == null) {
702 break;
703 }
704 time = current.timeSent;
705 }
706 return time;
707 }
708
709 public boolean wasMergedIntoPrevious() {
710 Message prev = this.prev();
711 return prev != null && prev.mergeable(this);
712 }
713
714 public boolean trusted() {
715 Contact contact = this.getContact();
716 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
717 }
718
719 public boolean fixCounterpart() {
720 Presences presences = conversation.getContact().getPresences();
721 if (counterpart != null && presences.has(counterpart.getResource())) {
722 return true;
723 } else if (presences.size() >= 1) {
724 try {
725 counterpart = Jid.of(conversation.getJid().getLocal(),
726 conversation.getJid().getDomain(),
727 presences.toResourceArray()[0]);
728 return true;
729 } catch (IllegalArgumentException e) {
730 counterpart = null;
731 return false;
732 }
733 } else {
734 counterpart = null;
735 return false;
736 }
737 }
738
739 public void setUuid(String uuid) {
740 this.uuid = uuid;
741 }
742
743 public String getEditedId() {
744 if (edits.size() > 0) {
745 return edits.get(edits.size() - 1).getEditedId();
746 } else {
747 throw new IllegalStateException("Attempting to store unedited message");
748 }
749 }
750
751 public String getEditedIdWireFormat() {
752 if (edits.size() > 0) {
753 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
754 } else {
755 throw new IllegalStateException("Attempting to store unedited message");
756 }
757 }
758
759 public void setOob(boolean isOob) {
760 this.oob = isOob;
761 }
762
763 public String getMimeType() {
764 String extension;
765 if (relativeFilePath != null) {
766 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
767 } else {
768 try {
769 final URL url = new URL(body.split("\n")[0]);
770 extension = MimeUtils.extractRelevantExtension(url);
771 } catch (MalformedURLException e) {
772 return null;
773 }
774 }
775 return MimeUtils.guessMimeTypeFromExtension(extension);
776 }
777
778 public synchronized boolean treatAsDownloadable() {
779 if (treatAsDownloadable == null) {
780 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
781 }
782 return treatAsDownloadable;
783 }
784
785 public synchronized boolean bodyIsOnlyEmojis() {
786 if (isEmojisOnly == null) {
787 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
788 }
789 return isEmojisOnly;
790 }
791
792 public synchronized boolean isGeoUri() {
793 if (isGeoUri == null) {
794 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
795 }
796 return isGeoUri;
797 }
798
799 public synchronized void resetFileParams() {
800 this.fileParams = null;
801 }
802
803 public synchronized FileParams getFileParams() {
804 if (fileParams == null) {
805 fileParams = new FileParams();
806 if (this.transferable != null) {
807 fileParams.size = this.transferable.getFileSize();
808 }
809 final String[] parts = body == null ? new String[0] : body.split("\\|");
810 switch (parts.length) {
811 case 1:
812 try {
813 fileParams.size = Long.parseLong(parts[0]);
814 } catch (NumberFormatException e) {
815 fileParams.url = parseUrl(parts[0]);
816 }
817 break;
818 case 5:
819 fileParams.runtime = parseInt(parts[4]);
820 case 4:
821 fileParams.width = parseInt(parts[2]);
822 fileParams.height = parseInt(parts[3]);
823 case 2:
824 fileParams.url = parseUrl(parts[0]);
825 fileParams.size = parseLong(parts[1]);
826 break;
827 case 3:
828 fileParams.size = parseLong(parts[0]);
829 fileParams.width = parseInt(parts[1]);
830 fileParams.height = parseInt(parts[2]);
831 break;
832 }
833 }
834 return fileParams;
835 }
836
837 private static long parseLong(String value) {
838 try {
839 return Long.parseLong(value);
840 } catch (NumberFormatException e) {
841 return 0;
842 }
843 }
844
845 private static int parseInt(String value) {
846 try {
847 return Integer.parseInt(value);
848 } catch (NumberFormatException e) {
849 return 0;
850 }
851 }
852
853 private static URL parseUrl(String value) {
854 try {
855 return new URL(value);
856 } catch (MalformedURLException e) {
857 return null;
858 }
859 }
860
861 public void untie() {
862 this.mNextMessage = null;
863 this.mPreviousMessage = null;
864 }
865
866 public boolean isPrivateMessage() {
867 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
868 }
869
870 public boolean isFileOrImage() {
871 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
872 }
873
874 public boolean hasFileOnRemoteHost() {
875 return isFileOrImage() && getFileParams().url != null;
876 }
877
878 public boolean needsUploading() {
879 return isFileOrImage() && getFileParams().url == null;
880 }
881
882 public class FileParams {
883 public URL url;
884 public long size = 0;
885 public int width = 0;
886 public int height = 0;
887 public int runtime = 0;
888 }
889
890 public void setFingerprint(String fingerprint) {
891 this.axolotlFingerprint = fingerprint;
892 }
893
894 public String getFingerprint() {
895 return axolotlFingerprint;
896 }
897
898 public boolean isTrusted() {
899 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
900 return s != null && s.isTrusted();
901 }
902
903 private int getPreviousEncryption() {
904 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
905 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
906 continue;
907 }
908 return iterator.getEncryption();
909 }
910 return ENCRYPTION_NONE;
911 }
912
913 private int getNextEncryption() {
914 if (this.conversation instanceof Conversation) {
915 Conversation conversation = (Conversation) this.conversation;
916 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
917 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
918 continue;
919 }
920 return iterator.getEncryption();
921 }
922 return conversation.getNextEncryption();
923 } else {
924 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
925 }
926 }
927
928 public boolean isValidInSession() {
929 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
930 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
931
932 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
933 || futureEncryption == ENCRYPTION_NONE
934 || pastEncryption != futureEncryption;
935
936 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
937 }
938
939 private static int getCleanedEncryption(int encryption) {
940 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
941 return ENCRYPTION_PGP;
942 }
943 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
944 return ENCRYPTION_AXOLOTL;
945 }
946 return encryption;
947 }
948
949 public static boolean configurePrivateMessage(final Message message) {
950 return configurePrivateMessage(message, false);
951 }
952
953 public static boolean configurePrivateFileMessage(final Message message) {
954 return configurePrivateMessage(message, true);
955 }
956
957 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
958 final Conversation conversation;
959 if (message.conversation instanceof Conversation) {
960 conversation = (Conversation) message.conversation;
961 } else {
962 return false;
963 }
964 if (conversation.getMode() == Conversation.MODE_MULTI) {
965 final Jid nextCounterpart = conversation.getNextCounterpart();
966 if (nextCounterpart != null) {
967 message.setCounterpart(nextCounterpart);
968 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
969 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
970 return true;
971 }
972 }
973 return false;
974 }
975}