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