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