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<Edited> 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 = Edited.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, Edited.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 this.edits.add(new Edited(edited, serverMsgId));
438 }
439
440 public boolean remoteMsgIdMatchInEdit(String id) {
441 for(Edited edit : this.edits) {
442 if (id.equals(edit.getEditedId())) {
443 return true;
444 }
445 }
446 return false;
447 }
448
449 public String getBodyLanguage() {
450 return this.bodyLanguage;
451 }
452
453 public void setBodyLanguage(String language) {
454 this.bodyLanguage = language;
455 }
456
457 public boolean edited() {
458 return this.edits.size() > 0;
459 }
460
461 public void setTrueCounterpart(Jid trueCounterpart) {
462 this.trueCounterpart = trueCounterpart;
463 }
464
465 public Jid getTrueCounterpart() {
466 return this.trueCounterpart;
467 }
468
469 public Transferable getTransferable() {
470 return this.transferable;
471 }
472
473 public synchronized void setTransferable(Transferable transferable) {
474 this.fileParams = null;
475 this.transferable = transferable;
476 }
477
478 public boolean addReadByMarker(ReadByMarker readByMarker) {
479 if (readByMarker.getRealJid() != null) {
480 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
481 return false;
482 }
483 } else if (readByMarker.getFullJid() != null) {
484 if (readByMarker.getFullJid().equals(counterpart)) {
485 return false;
486 }
487 }
488 if (this.readByMarkers.add(readByMarker)) {
489 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
490 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
491 while (iterator.hasNext()) {
492 ReadByMarker marker = iterator.next();
493 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
494 iterator.remove();
495 }
496 }
497 }
498 return true;
499 } else {
500 return false;
501 }
502 }
503
504 public Set<ReadByMarker> getReadByMarkers() {
505 return Collections.unmodifiableSet(this.readByMarkers);
506 }
507
508 boolean similar(Message message) {
509 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
510 return this.serverMsgId.equals(message.getServerMsgId()) || Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
511 } else if (Edited.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
512 return true;
513 } else if (this.body == null || this.counterpart == null) {
514 return false;
515 } else {
516 String body, otherBody;
517 if (this.hasFileOnRemoteHost()) {
518 body = getFileParams().url.toString();
519 otherBody = message.body == null ? null : message.body.trim();
520 } else {
521 body = this.body;
522 otherBody = message.body;
523 }
524 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
525 if (message.getRemoteMsgId() != null) {
526 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
527 if (hasUuid && matchingCounterpart && Edited.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
528 return true;
529 }
530 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
531 && matchingCounterpart
532 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
533 } else {
534 return this.remoteMsgId == null
535 && matchingCounterpart
536 && body.equals(otherBody)
537 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
538 }
539 }
540 }
541
542 public Message next() {
543 if (this.conversation instanceof Conversation) {
544 final Conversation conversation = (Conversation) this.conversation;
545 synchronized (conversation.messages) {
546 if (this.mNextMessage == null) {
547 int index = conversation.messages.indexOf(this);
548 if (index < 0 || index >= conversation.messages.size() - 1) {
549 this.mNextMessage = null;
550 } else {
551 this.mNextMessage = conversation.messages.get(index + 1);
552 }
553 }
554 return this.mNextMessage;
555 }
556 } else {
557 throw new AssertionError("Calling next should be disabled for stubs");
558 }
559 }
560
561 public Message prev() {
562 if (this.conversation instanceof Conversation) {
563 final Conversation conversation = (Conversation) this.conversation;
564 synchronized (conversation.messages) {
565 if (this.mPreviousMessage == null) {
566 int index = conversation.messages.indexOf(this);
567 if (index <= 0 || index > conversation.messages.size()) {
568 this.mPreviousMessage = null;
569 } else {
570 this.mPreviousMessage = conversation.messages.get(index - 1);
571 }
572 }
573 }
574 return this.mPreviousMessage;
575 } else {
576 throw new AssertionError("Calling prev should be disabled for stubs");
577 }
578 }
579
580 public boolean isLastCorrectableMessage() {
581 Message next = next();
582 while (next != null) {
583 if (next.isCorrectable()) {
584 return false;
585 }
586 next = next.next();
587 }
588 return isCorrectable();
589 }
590
591 private boolean isCorrectable() {
592 return getStatus() != STATUS_RECEIVED && !isCarbon();
593 }
594
595 public boolean mergeable(final Message message) {
596 return message != null &&
597 (message.getType() == Message.TYPE_TEXT &&
598 this.getTransferable() == null &&
599 message.getTransferable() == null &&
600 message.getEncryption() != Message.ENCRYPTION_PGP &&
601 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
602 this.getType() == message.getType() &&
603 //this.getStatus() == message.getStatus() &&
604 isStatusMergeable(this.getStatus(), message.getStatus()) &&
605 this.getEncryption() == message.getEncryption() &&
606 this.getCounterpart() != null &&
607 this.getCounterpart().equals(message.getCounterpart()) &&
608 this.edited() == message.edited() &&
609 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
610 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
611 !message.isGeoUri() &&
612 !this.isGeoUri() &&
613 !message.treatAsDownloadable() &&
614 !this.treatAsDownloadable() &&
615 !message.getBody().startsWith(ME_COMMAND) &&
616 !this.getBody().startsWith(ME_COMMAND) &&
617 !this.bodyIsOnlyEmojis() &&
618 !message.bodyIsOnlyEmojis() &&
619 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
620 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
621 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
622 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
623 );
624 }
625
626 private static boolean isStatusMergeable(int a, int b) {
627 return a == b || (
628 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
629 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
630 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
631 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
632 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
633 );
634 }
635
636 public void setCounterparts(List<MucOptions.User> counterparts) {
637 this.counterparts = counterparts;
638 }
639
640 public List<MucOptions.User> getCounterparts() {
641 return this.counterparts;
642 }
643
644 @Override
645 public int getAvatarBackgroundColor() {
646 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
647 return Color.TRANSPARENT;
648 } else {
649 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
650 }
651 }
652
653 public static class MergeSeparator {
654 }
655
656 public SpannableStringBuilder getMergedBody() {
657 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
658 Message current = this;
659 while (current.mergeable(current.next())) {
660 current = current.next();
661 if (current == null) {
662 break;
663 }
664 body.append("\n\n");
665 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
666 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
667 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
668 }
669 return body;
670 }
671
672 public boolean hasMeCommand() {
673 return this.body.trim().startsWith(ME_COMMAND);
674 }
675
676 public int getMergedStatus() {
677 int status = this.status;
678 Message current = this;
679 while (current.mergeable(current.next())) {
680 current = current.next();
681 if (current == null) {
682 break;
683 }
684 status = current.status;
685 }
686 return status;
687 }
688
689 public long getMergedTimeSent() {
690 long time = this.timeSent;
691 Message current = this;
692 while (current.mergeable(current.next())) {
693 current = current.next();
694 if (current == null) {
695 break;
696 }
697 time = current.timeSent;
698 }
699 return time;
700 }
701
702 public boolean wasMergedIntoPrevious() {
703 Message prev = this.prev();
704 return prev != null && prev.mergeable(this);
705 }
706
707 public boolean trusted() {
708 Contact contact = this.getContact();
709 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
710 }
711
712 public boolean fixCounterpart() {
713 Presences presences = conversation.getContact().getPresences();
714 if (counterpart != null && presences.has(counterpart.getResource())) {
715 return true;
716 } else if (presences.size() >= 1) {
717 try {
718 counterpart = Jid.of(conversation.getJid().getLocal(),
719 conversation.getJid().getDomain(),
720 presences.toResourceArray()[0]);
721 return true;
722 } catch (IllegalArgumentException e) {
723 counterpart = null;
724 return false;
725 }
726 } else {
727 counterpart = null;
728 return false;
729 }
730 }
731
732 public void setUuid(String uuid) {
733 this.uuid = uuid;
734 }
735
736 public String getEditedId() {
737 if (edits.size() > 0) {
738 return edits.get(edits.size() - 1).getEditedId();
739 } else {
740 throw new IllegalStateException("Attempting to store unedited message");
741 }
742 }
743
744 public String getEditedIdWireFormat() {
745 if (edits.size() > 0) {
746 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
747 } else {
748 throw new IllegalStateException("Attempting to store unedited message");
749 }
750 }
751
752 public void setOob(boolean isOob) {
753 this.oob = isOob;
754 }
755
756 public String getMimeType() {
757 String extension;
758 if (relativeFilePath != null) {
759 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
760 } else {
761 try {
762 final URL url = new URL(body.split("\n")[0]);
763 extension = MimeUtils.extractRelevantExtension(url);
764 } catch (MalformedURLException e) {
765 return null;
766 }
767 }
768 return MimeUtils.guessMimeTypeFromExtension(extension);
769 }
770
771 public synchronized boolean treatAsDownloadable() {
772 if (treatAsDownloadable == null) {
773 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
774 }
775 return treatAsDownloadable;
776 }
777
778 public synchronized boolean bodyIsOnlyEmojis() {
779 if (isEmojisOnly == null) {
780 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
781 }
782 return isEmojisOnly;
783 }
784
785 public synchronized boolean isGeoUri() {
786 if (isGeoUri == null) {
787 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
788 }
789 return isGeoUri;
790 }
791
792 public synchronized void resetFileParams() {
793 this.fileParams = null;
794 }
795
796 public synchronized FileParams getFileParams() {
797 if (fileParams == null) {
798 fileParams = new FileParams();
799 if (this.transferable != null) {
800 fileParams.size = this.transferable.getFileSize();
801 }
802 String parts[] = body == null ? new String[0] : body.split("\\|");
803 switch (parts.length) {
804 case 1:
805 try {
806 fileParams.size = Long.parseLong(parts[0]);
807 } catch (NumberFormatException e) {
808 fileParams.url = parseUrl(parts[0]);
809 }
810 break;
811 case 5:
812 fileParams.runtime = parseInt(parts[4]);
813 case 4:
814 fileParams.width = parseInt(parts[2]);
815 fileParams.height = parseInt(parts[3]);
816 case 2:
817 fileParams.url = parseUrl(parts[0]);
818 fileParams.size = parseLong(parts[1]);
819 break;
820 case 3:
821 fileParams.size = parseLong(parts[0]);
822 fileParams.width = parseInt(parts[1]);
823 fileParams.height = parseInt(parts[2]);
824 break;
825 }
826 }
827 return fileParams;
828 }
829
830 private static long parseLong(String value) {
831 try {
832 return Long.parseLong(value);
833 } catch (NumberFormatException e) {
834 return 0;
835 }
836 }
837
838 private static int parseInt(String value) {
839 try {
840 return Integer.parseInt(value);
841 } catch (NumberFormatException e) {
842 return 0;
843 }
844 }
845
846 private static URL parseUrl(String value) {
847 try {
848 return new URL(value);
849 } catch (MalformedURLException e) {
850 return null;
851 }
852 }
853
854 public void untie() {
855 this.mNextMessage = null;
856 this.mPreviousMessage = null;
857 }
858
859 public boolean isPrivateMessage() {
860 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
861 }
862
863 public boolean isFileOrImage() {
864 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
865 }
866
867 public boolean hasFileOnRemoteHost() {
868 return isFileOrImage() && getFileParams().url != null;
869 }
870
871 public boolean needsUploading() {
872 return isFileOrImage() && getFileParams().url == null;
873 }
874
875 public class FileParams {
876 public URL url;
877 public long size = 0;
878 public int width = 0;
879 public int height = 0;
880 public int runtime = 0;
881 }
882
883 public void setFingerprint(String fingerprint) {
884 this.axolotlFingerprint = fingerprint;
885 }
886
887 public String getFingerprint() {
888 return axolotlFingerprint;
889 }
890
891 public boolean isTrusted() {
892 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
893 return s != null && s.isTrusted();
894 }
895
896 private int getPreviousEncryption() {
897 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
898 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
899 continue;
900 }
901 return iterator.getEncryption();
902 }
903 return ENCRYPTION_NONE;
904 }
905
906 private int getNextEncryption() {
907 if (this.conversation instanceof Conversation) {
908 Conversation conversation = (Conversation) this.conversation;
909 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
910 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
911 continue;
912 }
913 return iterator.getEncryption();
914 }
915 return conversation.getNextEncryption();
916 } else {
917 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
918 }
919 }
920
921 public boolean isValidInSession() {
922 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
923 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
924
925 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
926 || futureEncryption == ENCRYPTION_NONE
927 || pastEncryption != futureEncryption;
928
929 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
930 }
931
932 private static int getCleanedEncryption(int encryption) {
933 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
934 return ENCRYPTION_PGP;
935 }
936 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
937 return ENCRYPTION_AXOLOTL;
938 }
939 return encryption;
940 }
941
942 public static boolean configurePrivateMessage(final Message message) {
943 return configurePrivateMessage(message, false);
944 }
945
946 public static boolean configurePrivateFileMessage(final Message message) {
947 return configurePrivateMessage(message, true);
948 }
949
950 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
951 final Conversation conversation;
952 if (message.conversation instanceof Conversation) {
953 conversation = (Conversation) message.conversation;
954 } else {
955 return false;
956 }
957 if (conversation.getMode() == Conversation.MODE_MULTI) {
958 final Jid nextCounterpart = conversation.getNextCounterpart();
959 if (nextCounterpart != null) {
960 message.setCounterpart(nextCounterpart);
961 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
962 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
963 return true;
964 }
965 }
966 return false;
967 }
968}