Message.java

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