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 de.gultsch.common.Patterns;
 12import eu.siacs.conversations.Config;
 13import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 14import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 15import eu.siacs.conversations.http.URL;
 16import eu.siacs.conversations.services.AvatarService;
 17import eu.siacs.conversations.ui.util.PresenceSelector;
 18import eu.siacs.conversations.utils.CryptoHelper;
 19import eu.siacs.conversations.utils.Emoticons;
 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()) < 20_000;
619            }
620        }
621    }
622
623    public Message next() {
624        if (this.conversation instanceof Conversation c) {
625            synchronized (c.messages) {
626                if (this.mNextMessage == null) {
627                    int index = c.messages.indexOf(this);
628                    if (index < 0 || index >= c.messages.size() - 1) {
629                        this.mNextMessage = null;
630                    } else {
631                        this.mNextMessage = c.messages.get(index + 1);
632                    }
633                }
634                return this.mNextMessage;
635            }
636        } else {
637            throw new AssertionError("Calling next should be disabled for stubs");
638        }
639    }
640
641    public Message prev() {
642        if (this.conversation instanceof Conversation c) {
643            synchronized (c.messages) {
644                if (this.mPreviousMessage == null) {
645                    int index = c.messages.indexOf(this);
646                    if (index <= 0 || index > c.messages.size()) {
647                        this.mPreviousMessage = null;
648                    } else {
649                        this.mPreviousMessage = c.messages.get(index - 1);
650                    }
651                }
652            }
653            return this.mPreviousMessage;
654        } else {
655            throw new AssertionError("Calling prev should be disabled for stubs");
656        }
657    }
658
659    public boolean isLastCorrectableMessage() {
660        Message next = next();
661        while (next != null) {
662            if (next.isEditable()) {
663                return false;
664            }
665            next = next.next();
666        }
667        return isEditable();
668    }
669
670    public boolean isEditable() {
671        return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
672    }
673
674    public void setCounterparts(List<MucOptions.User> counterparts) {
675        this.counterparts = counterparts;
676    }
677
678    public List<MucOptions.User> getCounterparts() {
679        return this.counterparts;
680    }
681
682    @Override
683    public int getAvatarBackgroundColor() {
684        if (type == Message.TYPE_STATUS
685                && getCounterparts() != null
686                && getCounterparts().size() > 1) {
687            return Color.TRANSPARENT;
688        } else {
689            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
690        }
691    }
692
693    @Override
694    public String getAvatarName() {
695        return UIHelper.getMessageDisplayName(this);
696    }
697
698    public boolean isOOb() {
699        return oob;
700    }
701
702    public void setOccupantId(final String id) {
703        this.occupantId = id;
704    }
705
706    public String getOccupantId() {
707        return this.occupantId;
708    }
709
710    public Collection<Reaction> getReactions() {
711        return this.reactions;
712    }
713
714    public Reaction.Aggregated getAggregatedReactions() {
715        return Reaction.aggregated(this.reactions);
716    }
717
718    public void setReactions(final Collection<Reaction> reactions) {
719        this.reactions = reactions;
720    }
721
722    public boolean hasMeCommand() {
723        return this.body.trim().startsWith(ME_COMMAND);
724    }
725
726    public boolean trusted() {
727        final var contact = this.getContact();
728        return status > STATUS_RECEIVED
729                || (contact != null && (contact.showInContactList() || contact.isSelf()));
730    }
731
732    public boolean fixCounterpart() {
733        final Presences presences = conversation.getContact().getPresences();
734        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
735            return true;
736        } else if (presences.isEmpty()) {
737            counterpart = null;
738            return false;
739        } else {
740            counterpart =
741                    PresenceSelector.getNextCounterpart(
742                            getContact(), presences.toResourceArray()[0]);
743            return true;
744        }
745    }
746
747    public void setUuid(String uuid) {
748        this.uuid = uuid;
749    }
750
751    public String getEditedId() {
752        if (this.edits.isEmpty()) {
753            throw new IllegalStateException("Attempting to access unedited message");
754        }
755        return edits.get(edits.size() - 1).getEditedId();
756    }
757
758    public String getEditedIdWireFormat() {
759        if (this.edits.isEmpty()) {
760            throw new IllegalStateException("Attempting to access unedited message");
761        }
762        return edits.get(0).getEditedId();
763    }
764
765    public void setOob(boolean isOob) {
766        this.oob = isOob;
767    }
768
769    public String getMimeType() {
770        String extension;
771        if (relativeFilePath != null) {
772            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
773        } else {
774            final String url = URL.tryParse(body.split("\n")[0]);
775            if (url == null) {
776                return null;
777            }
778            extension = MimeUtils.extractRelevantExtension(url);
779        }
780        return MimeUtils.guessMimeTypeFromExtension(extension);
781    }
782
783    public synchronized boolean treatAsDownloadable() {
784        if (treatAsDownloadable == null) {
785            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
786        }
787        return treatAsDownloadable;
788    }
789
790    public synchronized boolean bodyIsOnlyEmojis() {
791        if (isEmojisOnly == null) {
792            isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
793        }
794        return isEmojisOnly;
795    }
796
797    public synchronized boolean isGeoUri() {
798        if (isGeoUri == null) {
799            isGeoUri = Patterns.URI_GEO.matcher(body).matches();
800        }
801        return isGeoUri;
802    }
803
804    public synchronized void resetFileParams() {
805        this.fileParams = null;
806    }
807
808    public synchronized FileParams getFileParams() {
809        if (fileParams == null) {
810            fileParams = new FileParams();
811            if (this.transferable != null) {
812                fileParams.size = this.transferable.getFileSize();
813            }
814            final String[] parts = body == null ? new String[0] : body.split("\\|");
815            switch (parts.length) {
816                case 1:
817                    try {
818                        fileParams.size = Long.parseLong(parts[0]);
819                    } catch (final NumberFormatException e) {
820                        fileParams.url = URL.tryParse(parts[0]);
821                    }
822                    break;
823                case 5:
824                    fileParams.runtime = parseInt(parts[4]);
825                case 4:
826                    fileParams.width = parseInt(parts[2]);
827                    fileParams.height = parseInt(parts[3]);
828                case 2:
829                    fileParams.url = URL.tryParse(parts[0]);
830                    fileParams.size = Longs.tryParse(parts[1]);
831                    break;
832                case 3:
833                    fileParams.size = Longs.tryParse(parts[0]);
834                    fileParams.width = parseInt(parts[1]);
835                    fileParams.height = parseInt(parts[2]);
836                    break;
837            }
838        }
839        return fileParams;
840    }
841
842    private static int parseInt(String value) {
843        try {
844            return Integer.parseInt(value);
845        } catch (NumberFormatException e) {
846            return 0;
847        }
848    }
849
850    public void untie() {
851        this.mNextMessage = null;
852        this.mPreviousMessage = null;
853    }
854
855    public boolean isPrivateMessage() {
856        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
857    }
858
859    public boolean isFileOrImage() {
860        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
861    }
862
863    public boolean isTypeText() {
864        return type == TYPE_TEXT || type == TYPE_PRIVATE;
865    }
866
867    public boolean hasFileOnRemoteHost() {
868        return isFileOrImage() && getFileParams().url != null;
869    }
870
871    public boolean needsUploading() {
872        return isFileOrImage() && getFileParams().url == null;
873    }
874
875    public static class FileParams {
876        public String url;
877        public Long size = null;
878        public int width = 0;
879        public int height = 0;
880        public int runtime = 0;
881
882        public long getSize() {
883            return size == null ? 0 : size;
884        }
885    }
886
887    public void setFingerprint(String fingerprint) {
888        this.axolotlFingerprint = fingerprint;
889    }
890
891    public String getFingerprint() {
892        return axolotlFingerprint;
893    }
894
895    public boolean isTrusted() {
896        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
897        final FingerprintStatus s =
898                axolotlService != null
899                        ? axolotlService.getFingerprintTrust(axolotlFingerprint)
900                        : null;
901        return s != null && s.isTrusted();
902    }
903
904    private int getPreviousEncryption() {
905        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
906            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
907                continue;
908            }
909            return iterator.getEncryption();
910        }
911        return ENCRYPTION_NONE;
912    }
913
914    private int getNextEncryption() {
915        if (this.conversation instanceof Conversation c) {
916            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
917                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
918                    continue;
919                }
920                return iterator.getEncryption();
921            }
922            return c.getNextEncryption();
923        } else {
924            throw new AssertionError(
925                    "This should never be called since isInValidSession should be disabled for"
926                            + " stubs");
927        }
928    }
929
930    public boolean isValidInSession() {
931        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
932        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
933
934        boolean inUnencryptedSession =
935                pastEncryption == ENCRYPTION_NONE
936                        || futureEncryption == ENCRYPTION_NONE
937                        || pastEncryption != futureEncryption;
938
939        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
940    }
941
942    private static int getCleanedEncryption(int encryption) {
943        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
944            return ENCRYPTION_PGP;
945        }
946        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
947                || encryption == ENCRYPTION_AXOLOTL_FAILED) {
948            return ENCRYPTION_AXOLOTL;
949        }
950        return encryption;
951    }
952
953    public static void configurePrivateMessage(final Message message) {
954        configurePrivateMessage(message, false);
955    }
956
957    public static boolean configurePrivateFileMessage(final Message message) {
958        return configurePrivateMessage(message, true);
959    }
960
961    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
962        if (message.conversation instanceof Conversation conversation) {
963            if (conversation.getMode() == Conversation.MODE_MULTI) {
964                final Jid nextCounterpart = conversation.getNextCounterpart();
965                return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
966            }
967        }
968        return false;
969    }
970
971    public static void configurePrivateMessage(final Message message, final Jid counterpart) {
972        if (message.conversation instanceof Conversation conversation) {
973            configurePrivateMessage(conversation, message, counterpart, false);
974        }
975    }
976
977    private static boolean configurePrivateMessage(
978            final Conversation conversation,
979            final Message message,
980            final Jid counterpart,
981            final boolean isFile) {
982        if (counterpart == null) {
983            return false;
984        }
985        message.setCounterpart(counterpart);
986        final var mucOptions = conversation.getMucOptions();
987        if (counterpart.equals(mucOptions.getSelf().getFullJid())) {
988            message.setTrueCounterpart(conversation.getAccount().getJid().asBareJid());
989        } else {
990            final var user = mucOptions.findUserByFullJid(counterpart);
991            if (user != null) {
992                message.setTrueCounterpart(user.getRealJid());
993                message.setOccupantId(user.getOccupantId());
994            }
995        }
996        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
997        return true;
998    }
999}