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