Message.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.graphics.drawable.Drawable;
   6import android.graphics.Color;
   7import android.os.Build;
   8import android.text.Html;
   9import android.text.SpannableStringBuilder;
  10import android.text.Spanned;
  11import android.text.style.ImageSpan;
  12import android.text.style.ClickableSpan;
  13import android.util.Base64;
  14import android.util.Log;
  15import android.util.Pair;
  16import android.view.View;
  17
  18import com.cheogram.android.BobTransfer;
  19import com.cheogram.android.GetThumbnailForCid;
  20
  21import com.google.common.io.ByteSource;
  22import com.google.common.base.Strings;
  23import com.google.common.collect.ImmutableSet;
  24import com.google.common.primitives.Longs;
  25
  26import org.json.JSONException;
  27
  28import java.lang.ref.WeakReference;
  29import java.io.IOException;
  30import java.net.URI;
  31import java.net.URISyntaxException;
  32import java.time.Duration;
  33import java.security.NoSuchAlgorithmException;
  34import java.util.ArrayList;
  35import java.util.Arrays;
  36import java.util.Iterator;
  37import java.util.List;
  38import java.util.Set;
  39import java.util.stream.Collectors;
  40import java.util.concurrent.CopyOnWriteArraySet;
  41
  42import io.ipfs.cid.Cid;
  43
  44import eu.siacs.conversations.Config;
  45import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  46import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  47import eu.siacs.conversations.http.URL;
  48import eu.siacs.conversations.services.AvatarService;
  49import eu.siacs.conversations.ui.util.PresenceSelector;
  50import eu.siacs.conversations.utils.CryptoHelper;
  51import eu.siacs.conversations.utils.Emoticons;
  52import eu.siacs.conversations.utils.GeoHelper;
  53import eu.siacs.conversations.utils.MessageUtils;
  54import eu.siacs.conversations.utils.MimeUtils;
  55import eu.siacs.conversations.utils.UIHelper;
  56import eu.siacs.conversations.xmpp.Jid;
  57import eu.siacs.conversations.xml.Element;
  58import eu.siacs.conversations.xml.Namespace;
  59import eu.siacs.conversations.xml.Tag;
  60import eu.siacs.conversations.xml.XmlReader;
  61
  62public class Message extends AbstractEntity implements AvatarService.Avatarable {
  63
  64    public static final String TABLENAME = "messages";
  65
  66    public static final int STATUS_RECEIVED = 0;
  67    public static final int STATUS_UNSEND = 1;
  68    public static final int STATUS_SEND = 2;
  69    public static final int STATUS_SEND_FAILED = 3;
  70    public static final int STATUS_WAITING = 5;
  71    public static final int STATUS_OFFERED = 6;
  72    public static final int STATUS_SEND_RECEIVED = 7;
  73    public static final int STATUS_SEND_DISPLAYED = 8;
  74
  75    public static final int ENCRYPTION_NONE = 0;
  76    public static final int ENCRYPTION_PGP = 1;
  77    public static final int ENCRYPTION_OTR = 2;
  78    public static final int ENCRYPTION_DECRYPTED = 3;
  79    public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
  80    public static final int ENCRYPTION_AXOLOTL = 5;
  81    public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
  82    public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
  83
  84    public static final int TYPE_TEXT = 0;
  85    public static final int TYPE_IMAGE = 1;
  86    public static final int TYPE_FILE = 2;
  87    public static final int TYPE_STATUS = 3;
  88    public static final int TYPE_PRIVATE = 4;
  89    public static final int TYPE_PRIVATE_FILE = 5;
  90    public static final int TYPE_RTP_SESSION = 6;
  91
  92    public static final String CONVERSATION = "conversationUuid";
  93    public static final String COUNTERPART = "counterpart";
  94    public static final String TRUE_COUNTERPART = "trueCounterpart";
  95    public static final String BODY = "body";
  96    public static final String BODY_LANGUAGE = "bodyLanguage";
  97    public static final String TIME_SENT = "timeSent";
  98    public static final String ENCRYPTION = "encryption";
  99    public static final String STATUS = "status";
 100    public static final String TYPE = "type";
 101    public static final String CARBON = "carbon";
 102    public static final String OOB = "oob";
 103    public static final String EDITED = "edited";
 104    public static final String REMOTE_MSG_ID = "remoteMsgId";
 105    public static final String SERVER_MSG_ID = "serverMsgId";
 106    public static final String RELATIVE_FILE_PATH = "relativeFilePath";
 107    public static final String FINGERPRINT = "axolotl_fingerprint";
 108    public static final String READ = "read";
 109    public static final String ERROR_MESSAGE = "errorMsg";
 110    public static final String READ_BY_MARKERS = "readByMarkers";
 111    public static final String MARKABLE = "markable";
 112    public static final String DELETED = "deleted";
 113    public static final String ME_COMMAND = "/me ";
 114
 115    public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
 116
 117
 118    public boolean markable = false;
 119    protected String conversationUuid;
 120    protected Jid counterpart;
 121    protected Jid trueCounterpart;
 122    protected String body;
 123    protected String subject;
 124    protected String encryptedBody;
 125    protected long timeSent;
 126    protected long timeReceived;
 127    protected int encryption;
 128    protected int status;
 129    protected int type;
 130    protected boolean deleted = false;
 131    protected boolean carbon = false;
 132    private boolean oob = false;
 133    protected List<Element> payloads = new ArrayList<>();
 134    protected List<Edit> edits = new ArrayList<>();
 135    protected String relativeFilePath;
 136    protected boolean read = true;
 137    protected String remoteMsgId = null;
 138    private String bodyLanguage = null;
 139    protected String serverMsgId = null;
 140    private final Conversational conversation;
 141    protected Transferable transferable = null;
 142    private Message mNextMessage = null;
 143    private Message mPreviousMessage = null;
 144    private String axolotlFingerprint = null;
 145    private String errorMessage = null;
 146    private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
 147
 148    private Boolean isGeoUri = null;
 149    private Boolean isEmojisOnly = null;
 150    private Boolean treatAsDownloadable = null;
 151    private FileParams fileParams = null;
 152    private List<MucOptions.User> counterparts;
 153    private WeakReference<MucOptions.User> user;
 154
 155    protected Message(Conversational conversation) {
 156        this.conversation = conversation;
 157    }
 158
 159    public Message(Conversational conversation, String body, int encryption) {
 160        this(conversation, body, encryption, STATUS_UNSEND);
 161    }
 162
 163    public Message(Conversational conversation, String body, int encryption, int status) {
 164        this(conversation, java.util.UUID.randomUUID().toString(),
 165                conversation.getUuid(),
 166                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 167                null,
 168                body,
 169                System.currentTimeMillis(),
 170                encryption,
 171                status,
 172                TYPE_TEXT,
 173                false,
 174                null,
 175                null,
 176                null,
 177                null,
 178                true,
 179                null,
 180                false,
 181                null,
 182                null,
 183                false,
 184                false,
 185                null,
 186                System.currentTimeMillis(),
 187                null,
 188                null,
 189                null);
 190    }
 191
 192    public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
 193        this(conversation, java.util.UUID.randomUUID().toString(),
 194                conversation.getUuid(),
 195                conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 196                null,
 197                null,
 198                System.currentTimeMillis(),
 199                Message.ENCRYPTION_NONE,
 200                status,
 201                type,
 202                false,
 203                remoteMsgId,
 204                null,
 205                null,
 206                null,
 207                true,
 208                null,
 209                false,
 210                null,
 211                null,
 212                false,
 213                false,
 214                null,
 215                System.currentTimeMillis(),
 216                null,
 217                null,
 218                null);
 219    }
 220
 221    protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
 222                      final Jid trueCounterpart, final String body, final long timeSent,
 223                      final int encryption, final int status, final int type, final boolean carbon,
 224                      final String remoteMsgId, final String relativeFilePath,
 225                      final String serverMsgId, final String fingerprint, final boolean read,
 226                      final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
 227                      final boolean markable, final boolean deleted, final String bodyLanguage, final long timeReceived, final String subject, final String fileParams, final List<Element> payloads) {
 228        this.conversation = conversation;
 229        this.uuid = uuid;
 230        this.conversationUuid = conversationUUid;
 231        this.counterpart = counterpart;
 232        this.trueCounterpart = trueCounterpart;
 233        this.body = body == null ? "" : body;
 234        this.timeSent = timeSent;
 235        this.encryption = encryption;
 236        this.status = status;
 237        this.type = type;
 238        this.carbon = carbon;
 239        this.remoteMsgId = remoteMsgId;
 240        this.relativeFilePath = relativeFilePath;
 241        this.serverMsgId = serverMsgId;
 242        this.axolotlFingerprint = fingerprint;
 243        this.read = read;
 244        this.edits = Edit.fromJson(edited);
 245        this.oob = oob;
 246        this.errorMessage = errorMessage;
 247        this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
 248        this.markable = markable;
 249        this.deleted = deleted;
 250        this.bodyLanguage = bodyLanguage;
 251        this.timeReceived = timeReceived;
 252        this.subject = subject;
 253        if (payloads != null) this.payloads = payloads;
 254        if (fileParams != null && getSims().isEmpty()) this.fileParams = new FileParams(fileParams);
 255    }
 256
 257    public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
 258        String payloadsStr = cursor.getString(cursor.getColumnIndex("payloads"));
 259        List<Element> payloads = new ArrayList<>();
 260        if (payloadsStr != null) {
 261            final XmlReader xmlReader = new XmlReader();
 262            xmlReader.setInputStream(ByteSource.wrap(payloadsStr.getBytes()).openStream());
 263            Tag tag;
 264            while ((tag = xmlReader.readTag()) != null) {
 265                payloads.add(xmlReader.readElement(tag));
 266            }
 267        }
 268
 269        return new Message(conversation,
 270                cursor.getString(cursor.getColumnIndex(UUID)),
 271                cursor.getString(cursor.getColumnIndex(CONVERSATION)),
 272                fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
 273                fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
 274                cursor.getString(cursor.getColumnIndex(BODY)),
 275                cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
 276                cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
 277                cursor.getInt(cursor.getColumnIndex(STATUS)),
 278                cursor.getInt(cursor.getColumnIndex(TYPE)),
 279                cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
 280                cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
 281                cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
 282                cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
 283                cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
 284                cursor.getInt(cursor.getColumnIndex(READ)) > 0,
 285                cursor.getString(cursor.getColumnIndex(EDITED)),
 286                cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
 287                cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
 288                ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
 289                cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
 290                cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
 291                cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
 292                cursor.getLong(cursor.getColumnIndex(cursor.isNull(cursor.getColumnIndex("timeReceived")) ? TIME_SENT : "timeReceived")),
 293                cursor.getString(cursor.getColumnIndex("subject")),
 294                cursor.getString(cursor.getColumnIndex("fileParams")),
 295                payloads
 296        );
 297    }
 298
 299    private static Jid fromString(String value) {
 300        try {
 301            if (value != null) {
 302                return Jid.of(value);
 303            }
 304        } catch (IllegalArgumentException e) {
 305            return null;
 306        }
 307        return null;
 308    }
 309
 310    public static Message createStatusMessage(Conversation conversation, String body) {
 311        final Message message = new Message(conversation);
 312        message.setType(Message.TYPE_STATUS);
 313        message.setStatus(Message.STATUS_RECEIVED);
 314        message.body = body;
 315        return message;
 316    }
 317
 318    public static Message createLoadMoreMessage(Conversation conversation) {
 319        final Message message = new Message(conversation);
 320        message.setType(Message.TYPE_STATUS);
 321        message.body = "LOAD_MORE";
 322        return message;
 323    }
 324
 325    public ContentValues getCheogramContentValues() {
 326        ContentValues values = new ContentValues();
 327        values.put(UUID, uuid);
 328        values.put("subject", subject);
 329        values.put("fileParams", fileParams == null ? null : fileParams.toString());
 330        if (fileParams != null) {
 331            List<Element> sims = getSims();
 332            if (sims.isEmpty()) {
 333                addPayload(fileParams.toSims());
 334            } else {
 335                sims.get(0).replaceChildren(fileParams.toSims().getChildren());
 336            }
 337        }
 338        values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
 339        return values;
 340    }
 341
 342    @Override
 343    public ContentValues getContentValues() {
 344        ContentValues values = new ContentValues();
 345        values.put(UUID, uuid);
 346        values.put(CONVERSATION, conversationUuid);
 347        if (counterpart == null) {
 348            values.putNull(COUNTERPART);
 349        } else {
 350            values.put(COUNTERPART, counterpart.toString());
 351        }
 352        if (trueCounterpart == null) {
 353            values.putNull(TRUE_COUNTERPART);
 354        } else {
 355            values.put(TRUE_COUNTERPART, trueCounterpart.toString());
 356        }
 357        values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
 358        values.put(TIME_SENT, timeSent);
 359        values.put(ENCRYPTION, encryption);
 360        values.put(STATUS, status);
 361        values.put(TYPE, type);
 362        values.put(CARBON, carbon ? 1 : 0);
 363        values.put(REMOTE_MSG_ID, remoteMsgId);
 364        values.put(RELATIVE_FILE_PATH, relativeFilePath);
 365        values.put(SERVER_MSG_ID, serverMsgId);
 366        values.put(FINGERPRINT, axolotlFingerprint);
 367        values.put(READ, read ? 1 : 0);
 368        try {
 369            values.put(EDITED, Edit.toJson(edits));
 370        } catch (JSONException e) {
 371            Log.e(Config.LOGTAG, "error persisting json for edits", e);
 372        }
 373        values.put(OOB, oob ? 1 : 0);
 374        values.put(ERROR_MESSAGE, errorMessage);
 375        values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
 376        values.put(MARKABLE, markable ? 1 : 0);
 377        values.put(DELETED, deleted ? 1 : 0);
 378        values.put(BODY_LANGUAGE, bodyLanguage);
 379        return values;
 380    }
 381
 382    public String getConversationUuid() {
 383        return conversationUuid;
 384    }
 385
 386    public Conversational getConversation() {
 387        return this.conversation;
 388    }
 389
 390    public Jid getCounterpart() {
 391        return counterpart;
 392    }
 393
 394    public void setCounterpart(final Jid counterpart) {
 395        this.counterpart = counterpart;
 396    }
 397
 398    public Contact getContact() {
 399        if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
 400            if (this.trueCounterpart != null) {
 401                return this.conversation.getAccount().getRoster()
 402                           .getContact(this.trueCounterpart);
 403            }
 404
 405            return this.conversation.getContact();
 406        } else {
 407            if (this.trueCounterpart == null) {
 408                return null;
 409            } else {
 410                return this.conversation.getAccount().getRoster()
 411                        .getContactFromContactList(this.trueCounterpart);
 412            }
 413        }
 414    }
 415
 416    public String getBody() {
 417        StringBuilder body = new StringBuilder(this.body);
 418
 419        List<Element> fallbacks = getFallbacks();
 420        List<Pair<Integer, Integer>> spans = new ArrayList<>();
 421        for (Element fallback : fallbacks) {
 422            for (Element span : fallback.getChildren()) {
 423                if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
 424                if (span.getAttribute("start") == null || span.getAttribute("end") == null) return "";
 425                spans.add(new Pair(parseInt(span.getAttribute("start")), parseInt(span.getAttribute("end"))));
 426            }
 427        }
 428        // Do them in reverse order so that span deletions don't affect the indexes of other spans
 429        spans.sort((x, y) -> y.first.compareTo(x.first));
 430        try {
 431            for (Pair<Integer, Integer> span : spans) {
 432                body.delete(span.first, span.second);
 433            }
 434        } catch (final StringIndexOutOfBoundsException e) { spans.clear(); }
 435
 436        if (spans.isEmpty() && getOob() != null) {
 437            return body.toString().replace(getOob().toString(), "");
 438        } else {
 439            return body.toString();
 440        }
 441    }
 442
 443    public synchronized void setBody(String body) {
 444        if (body == null) {
 445            throw new Error("You should not set the message body to null");
 446        }
 447        this.body = body;
 448        this.isGeoUri = null;
 449        this.isEmojisOnly = null;
 450        this.treatAsDownloadable = null;
 451    }
 452
 453    public String getSubject() {
 454        return subject;
 455    }
 456
 457    public synchronized void setSubject(String subject) {
 458        this.subject = subject;
 459    }
 460
 461    public Element getThread() {
 462        if (this.payloads == null) return null;
 463
 464        for (Element el : this.payloads) {
 465            if (el.getName().equals("thread") && el.getNamespace().equals("jabber:client")) {
 466                return el;
 467            }
 468        }
 469
 470        return null;
 471    }
 472
 473    public void setThread(Element thread) {
 474        payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
 475        addPayload(thread);
 476    }
 477
 478    public void setMucUser(MucOptions.User user) {
 479        this.user = new WeakReference<>(user);
 480    }
 481
 482    public boolean sameMucUser(Message otherMessage) {
 483        final MucOptions.User thisUser = this.user == null ? null : this.user.get();
 484        final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
 485        return thisUser != null && thisUser == otherUser;
 486    }
 487
 488    public String getErrorMessage() {
 489        return errorMessage;
 490    }
 491
 492    public boolean setErrorMessage(String message) {
 493        boolean changed = (message != null && !message.equals(errorMessage))
 494                || (message == null && errorMessage != null);
 495        this.errorMessage = message;
 496        return changed;
 497    }
 498
 499    public long getTimeReceived() {
 500        return timeReceived;
 501    }
 502
 503    public long getTimeSent() {
 504        return timeSent;
 505    }
 506
 507    public int getEncryption() {
 508        return encryption;
 509    }
 510
 511    public void setEncryption(int encryption) {
 512        this.encryption = encryption;
 513    }
 514
 515    public int getStatus() {
 516        return status;
 517    }
 518
 519    public void setStatus(int status) {
 520        this.status = status;
 521    }
 522
 523    public String getRelativeFilePath() {
 524        return this.relativeFilePath;
 525    }
 526
 527    public void setRelativeFilePath(String path) {
 528        this.relativeFilePath = path;
 529    }
 530
 531    public String getRemoteMsgId() {
 532        return this.remoteMsgId;
 533    }
 534
 535    public void setRemoteMsgId(String id) {
 536        this.remoteMsgId = id;
 537    }
 538
 539    public String getServerMsgId() {
 540        return this.serverMsgId;
 541    }
 542
 543    public void setServerMsgId(String id) {
 544        this.serverMsgId = id;
 545    }
 546
 547    public boolean isRead() {
 548        return this.read;
 549    }
 550
 551    public boolean isDeleted() {
 552        return this.deleted;
 553    }
 554
 555    public Element getModerated() {
 556        if (this.payloads == null) return null;
 557
 558        for (Element el : this.payloads) {
 559            if (el.getName().equals("moderated") && el.getNamespace().equals("urn:xmpp:message-moderate:0")) {
 560                return el;
 561            }
 562        }
 563
 564        return null;
 565    }
 566
 567    public void setDeleted(boolean deleted) {
 568        this.deleted = deleted;
 569    }
 570
 571    public void markRead() {
 572        this.read = true;
 573    }
 574
 575    public void markUnread() {
 576        this.read = false;
 577    }
 578
 579    public void setTime(long time) {
 580        this.timeSent = time;
 581    }
 582
 583    public void setTimeReceived(long time) {
 584        this.timeReceived = time;
 585    }
 586
 587    public String getEncryptedBody() {
 588        return this.encryptedBody;
 589    }
 590
 591    public void setEncryptedBody(String body) {
 592        this.encryptedBody = body;
 593    }
 594
 595    public int getType() {
 596        return this.type;
 597    }
 598
 599    public void setType(int type) {
 600        this.type = type;
 601    }
 602
 603    public boolean isCarbon() {
 604        return carbon;
 605    }
 606
 607    public void setCarbon(boolean carbon) {
 608        this.carbon = carbon;
 609    }
 610
 611    public void putEdited(String edited, String serverMsgId) {
 612        final Edit edit = new Edit(edited, serverMsgId);
 613        if (this.edits.size() < 128 && !this.edits.contains(edit)) {
 614            this.edits.add(edit);
 615        }
 616    }
 617
 618    boolean remoteMsgIdMatchInEdit(String id) {
 619        for (Edit edit : this.edits) {
 620            if (id.equals(edit.getEditedId())) {
 621                return true;
 622            }
 623        }
 624        return false;
 625    }
 626
 627    public String getBodyLanguage() {
 628        return this.bodyLanguage;
 629    }
 630
 631    public void setBodyLanguage(String language) {
 632        this.bodyLanguage = language;
 633    }
 634
 635    public boolean edited() {
 636        return this.edits.size() > 0;
 637    }
 638
 639    public void setTrueCounterpart(Jid trueCounterpart) {
 640        this.trueCounterpart = trueCounterpart;
 641    }
 642
 643    public Jid getTrueCounterpart() {
 644        return this.trueCounterpart;
 645    }
 646
 647    public Transferable getTransferable() {
 648        return this.transferable;
 649    }
 650
 651    public synchronized void setTransferable(Transferable transferable) {
 652        this.transferable = transferable;
 653    }
 654
 655    public boolean addReadByMarker(ReadByMarker readByMarker) {
 656        if (readByMarker.getRealJid() != null) {
 657            if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
 658                return false;
 659            }
 660        } else if (readByMarker.getFullJid() != null) {
 661            if (readByMarker.getFullJid().equals(counterpart)) {
 662                return false;
 663            }
 664        }
 665        if (this.readByMarkers.add(readByMarker)) {
 666            if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
 667                Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
 668                while (iterator.hasNext()) {
 669                    ReadByMarker marker = iterator.next();
 670                    if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
 671                        iterator.remove();
 672                    }
 673                }
 674            }
 675            return true;
 676        } else {
 677            return false;
 678        }
 679    }
 680
 681    public Set<ReadByMarker> getReadByMarkers() {
 682        return ImmutableSet.copyOf(this.readByMarkers);
 683    }
 684
 685    boolean similar(Message message) {
 686        if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 687            return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 688        } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 689            return true;
 690        } else if (this.body == null || this.counterpart == null) {
 691            return false;
 692        } else {
 693            String body, otherBody;
 694            if (this.hasFileOnRemoteHost()) {
 695                body = getFileParams().url;
 696                otherBody = message.body == null ? null : message.body.trim();
 697            } else {
 698                body = this.body;
 699                otherBody = message.body;
 700            }
 701            final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
 702            if (message.getRemoteMsgId() != null) {
 703                final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
 704                if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
 705                    return true;
 706                }
 707                return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
 708                        && matchingCounterpart
 709                        && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
 710            } else {
 711                return this.remoteMsgId == null
 712                        && matchingCounterpart
 713                        && body.equals(otherBody)
 714                        && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
 715            }
 716        }
 717    }
 718
 719    public Message next() {
 720        if (this.conversation instanceof Conversation) {
 721            final Conversation conversation = (Conversation) this.conversation;
 722            synchronized (conversation.messages) {
 723                if (this.mNextMessage == null) {
 724                    int index = conversation.messages.indexOf(this);
 725                    if (index < 0 || index >= conversation.messages.size() - 1) {
 726                        this.mNextMessage = null;
 727                    } else {
 728                        this.mNextMessage = conversation.messages.get(index + 1);
 729                    }
 730                }
 731                return this.mNextMessage;
 732            }
 733        } else {
 734            throw new AssertionError("Calling next should be disabled for stubs");
 735        }
 736    }
 737
 738    public Message prev() {
 739        if (this.conversation instanceof Conversation) {
 740            final Conversation conversation = (Conversation) this.conversation;
 741            synchronized (conversation.messages) {
 742                if (this.mPreviousMessage == null) {
 743                    int index = conversation.messages.indexOf(this);
 744                    if (index <= 0 || index > conversation.messages.size()) {
 745                        this.mPreviousMessage = null;
 746                    } else {
 747                        this.mPreviousMessage = conversation.messages.get(index - 1);
 748                    }
 749                }
 750            }
 751            return this.mPreviousMessage;
 752        } else {
 753            throw new AssertionError("Calling prev should be disabled for stubs");
 754        }
 755    }
 756
 757    public boolean isLastCorrectableMessage() {
 758        Message next = next();
 759        while (next != null) {
 760            if (next.isEditable()) {
 761                return false;
 762            }
 763            next = next.next();
 764        }
 765        return isEditable();
 766    }
 767
 768    public boolean isEditable() {
 769        return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
 770    }
 771
 772    public boolean mergeable(final Message message) {
 773        return message != null &&
 774                (message.getType() == Message.TYPE_TEXT &&
 775                        this.getTransferable() == null &&
 776                        message.getTransferable() == null &&
 777                        message.getEncryption() != Message.ENCRYPTION_PGP &&
 778                        message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
 779                        this.getType() == message.getType() &&
 780                        this.getSubject() != null &&
 781                        isStatusMergeable(this.getStatus(), message.getStatus()) &&
 782                        isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
 783                        this.getCounterpart() != null &&
 784                        this.getCounterpart().equals(message.getCounterpart()) &&
 785                        this.edited() == message.edited() &&
 786                        (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 787                        this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 788                        !message.isGeoUri() &&
 789                        !this.isGeoUri() &&
 790                        !message.isOOb() &&
 791                        !this.isOOb() &&
 792                        !message.treatAsDownloadable() &&
 793                        !this.treatAsDownloadable() &&
 794                        !message.hasMeCommand() &&
 795                        !this.hasMeCommand() &&
 796                        !this.bodyIsOnlyEmojis() &&
 797                        !message.bodyIsOnlyEmojis() &&
 798                        ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 799                        UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 800                        this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 801                        !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 802                );
 803    }
 804
 805    private static boolean isStatusMergeable(int a, int b) {
 806        return a == b || (
 807                (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 808                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 809                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 810                        || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 811                        || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 812        );
 813    }
 814
 815    private static boolean isEncryptionMergeable(final int a, final int b) {
 816        return a == b
 817                && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
 818                        .contains(a);
 819    }
 820
 821    public void setCounterparts(List<MucOptions.User> counterparts) {
 822        this.counterparts = counterparts;
 823    }
 824
 825    public List<MucOptions.User> getCounterparts() {
 826        return this.counterparts;
 827    }
 828
 829    @Override
 830    public int getAvatarBackgroundColor() {
 831        if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 832            return Color.TRANSPARENT;
 833        } else {
 834            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 835        }
 836    }
 837
 838    @Override
 839    public String getAvatarName() {
 840        return UIHelper.getMessageDisplayName(this);
 841    }
 842
 843    public boolean isOOb() {
 844        return oob || getFileParams().url != null;
 845    }
 846
 847    public static class MergeSeparator {
 848    }
 849
 850    public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
 851        final Element html = getHtml();
 852        if (html == null || Build.VERSION.SDK_INT < 24) {
 853            return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
 854        } else {
 855            SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
 856                MessageUtils.filterLtrRtl(html.toString()).trim(),
 857                Html.FROM_HTML_MODE_COMPACT,
 858                (source) -> {
 859                   try {
 860                       if (thumbnailer == null) return fallbackImg;
 861                       Cid cid = BobTransfer.cid(new URI(source));
 862                       if (cid == null) return fallbackImg;
 863                       Drawable thumbnail = thumbnailer.getThumbnail(cid);
 864                       if (thumbnail == null) return fallbackImg;
 865                       return thumbnail;
 866                   } catch (final URISyntaxException e) {
 867                       return fallbackImg;
 868                   }
 869                },
 870                (opening, tag, output, xmlReader) -> {}
 871            ));
 872
 873            // Make images clickable and long-clickable with BetterLinkMovementMethod
 874            ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
 875            for (ImageSpan span : imageSpans) {
 876                final int start = spannable.getSpanStart(span);
 877                final int end = spannable.getSpanEnd(span);
 878
 879                ClickableSpan click_span = new ClickableSpan() {
 880                    @Override
 881                    public void onClick(View widget) { }
 882                };
 883
 884                spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 885            }
 886
 887            // https://stackoverflow.com/a/10187511/8611
 888            int i = spannable.length();
 889            while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
 890            return (SpannableStringBuilder) spannable.subSequence(0, i+1);
 891        }
 892    }
 893
 894    public SpannableStringBuilder getMergedBody() {
 895        return getMergedBody(null, null);
 896    }
 897
 898    public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
 899        SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
 900        Message current = this;
 901        while (current.mergeable(current.next())) {
 902            current = current.next();
 903            if (current == null) {
 904                break;
 905            }
 906            body.append("\n\n");
 907            body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 908                    SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 909            body.append(current.getSpannableBody(thumbnailer, fallbackImg));
 910        }
 911        return body;
 912    }
 913
 914    public boolean hasMeCommand() {
 915        return this.body.trim().startsWith(ME_COMMAND);
 916    }
 917
 918    public int getMergedStatus() {
 919        int status = this.status;
 920        Message current = this;
 921        while (current.mergeable(current.next())) {
 922            current = current.next();
 923            if (current == null) {
 924                break;
 925            }
 926            status = current.status;
 927        }
 928        return status;
 929    }
 930
 931    public long getMergedTimeSent() {
 932        long time = this.timeSent;
 933        Message current = this;
 934        while (current.mergeable(current.next())) {
 935            current = current.next();
 936            if (current == null) {
 937                break;
 938            }
 939            time = current.timeSent;
 940        }
 941        return time;
 942    }
 943
 944    public boolean wasMergedIntoPrevious() {
 945        Message prev = this.prev();
 946        return prev != null && prev.mergeable(this);
 947    }
 948
 949    public boolean trusted() {
 950        Contact contact = this.getContact();
 951        return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
 952    }
 953
 954    public boolean fixCounterpart() {
 955        final Presences presences = conversation.getContact().getPresences();
 956        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
 957            return true;
 958        } else if (presences.size() >= 1) {
 959            counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
 960            return true;
 961        } else {
 962            counterpart = null;
 963            return false;
 964        }
 965    }
 966
 967    public void setUuid(String uuid) {
 968        this.uuid = uuid;
 969    }
 970
 971    public String getEditedId() {
 972        if (edits.size() > 0) {
 973            return edits.get(edits.size() - 1).getEditedId();
 974        } else {
 975            throw new IllegalStateException("Attempting to store unedited message");
 976        }
 977    }
 978
 979    public String getEditedIdWireFormat() {
 980        if (edits.size() > 0) {
 981            return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
 982        } else {
 983            throw new IllegalStateException("Attempting to store unedited message");
 984        }
 985    }
 986
 987    public URI getOob() {
 988        final String url = getFileParams().url;
 989        try {
 990            return url == null ? null : new URI(url);
 991        } catch (final URISyntaxException e) {
 992            return null;
 993        }
 994    }
 995
 996    public void clearPayloads() {
 997        this.payloads.clear();
 998    }
 999
1000    public void addPayload(Element el) {
1001        if (el == null) return;
1002
1003        this.payloads.add(el);
1004    }
1005
1006    public List<Element> getPayloads() {
1007       return new ArrayList<>(this.payloads);
1008    }
1009
1010    public List<Element> getFallbacks() {
1011        List<Element> fallbacks = new ArrayList<>();
1012
1013        if (this.payloads == null) return fallbacks;
1014
1015        for (Element el : this.payloads) {
1016            if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
1017                final String fallbackFor = el.getAttribute("for");
1018                if (fallbackFor == null) continue;
1019                if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
1020                    fallbacks.add(el);
1021                }
1022            }
1023        }
1024
1025        return fallbacks;
1026    }
1027
1028    public Element getHtml() {
1029        if (this.payloads == null) return null;
1030
1031        for (Element el : this.payloads) {
1032            if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
1033                return el.getChildren().get(0);
1034            }
1035        }
1036
1037        return null;
1038   }
1039
1040    public List<Element> getCommands() {
1041        if (this.payloads == null) return null;
1042
1043        for (Element el : this.payloads) {
1044            if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
1045                return el.getChildren();
1046            }
1047        }
1048
1049        return null;
1050    }
1051
1052    public String getMimeType() {
1053        String extension;
1054        if (relativeFilePath != null) {
1055            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
1056        } else {
1057            final String url = URL.tryParse(getOob() == null ? body.split("\n")[0] : getOob().toString());
1058            if (url == null) {
1059                return null;
1060            }
1061            extension = MimeUtils.extractRelevantExtension(url);
1062        }
1063        return MimeUtils.guessMimeTypeFromExtension(extension);
1064    }
1065
1066    public synchronized boolean treatAsDownloadable() {
1067        if (treatAsDownloadable == null) {
1068            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb());
1069        }
1070        return treatAsDownloadable;
1071    }
1072
1073    public synchronized boolean bodyIsOnlyEmojis() {
1074        if (isEmojisOnly == null) {
1075            isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
1076        }
1077        return isEmojisOnly;
1078    }
1079
1080    public synchronized boolean isGeoUri() {
1081        if (isGeoUri == null) {
1082            isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
1083        }
1084        return isGeoUri;
1085    }
1086
1087    protected List<Element> getSims() {
1088        return payloads.stream().filter(el ->
1089            el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0") &&
1090            el.findChild("media-sharing", "urn:xmpp:sims:1") != null
1091        ).collect(Collectors.toList());
1092    }
1093
1094    public synchronized void resetFileParams() {
1095        this.fileParams = null;
1096    }
1097
1098    public synchronized void setFileParams(FileParams fileParams) {
1099        if (fileParams != null && this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
1100            fileParams.sims = this.fileParams.sims;
1101        }
1102        this.fileParams = fileParams;
1103    }
1104
1105    public synchronized FileParams getFileParams() {
1106        if (fileParams == null) {
1107            List<Element> sims = getSims();
1108            fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1109            if (this.transferable != null) {
1110                fileParams.size = this.transferable.getFileSize();
1111            }
1112        }
1113
1114        return fileParams;
1115    }
1116
1117    private static int parseInt(String value) {
1118        try {
1119            return Integer.parseInt(value);
1120        } catch (NumberFormatException e) {
1121            return 0;
1122        }
1123    }
1124
1125    public void untie() {
1126        this.mNextMessage = null;
1127        this.mPreviousMessage = null;
1128    }
1129
1130    public boolean isPrivateMessage() {
1131        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1132    }
1133
1134    public boolean isFileOrImage() {
1135        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1136    }
1137
1138
1139    public boolean isTypeText() {
1140        return type == TYPE_TEXT || type == TYPE_PRIVATE;
1141    }
1142
1143    public boolean hasFileOnRemoteHost() {
1144        return isFileOrImage() && getFileParams().url != null;
1145    }
1146
1147    public boolean needsUploading() {
1148        return isFileOrImage() && getFileParams().url == null;
1149    }
1150
1151    public static class FileParams {
1152        public String url;
1153        public Long size = null;
1154        public int width = 0;
1155        public int height = 0;
1156        public int runtime = 0;
1157        public Element sims = null;
1158
1159        public FileParams() { }
1160
1161        public FileParams(Element el) {
1162            if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1163                this.url = el.findChildContent("url", Namespace.OOB);
1164            }
1165            if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1166                sims = el;
1167                final String refUri = el.getAttribute("uri");
1168                if (refUri != null) url = refUri;
1169                final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1170                if (mediaSharing != null) {
1171                    Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1172                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1173                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1174                    if (file != null) {
1175                        String sizeS = file.findChildContent("size", file.getNamespace());
1176                        if (sizeS != null) size = new Long(sizeS);
1177                        String widthS = file.findChildContent("width", "https://schema.org/");
1178                        if (widthS != null) width = parseInt(widthS);
1179                        String heightS = file.findChildContent("height", "https://schema.org/");
1180                        if (heightS != null) height = parseInt(heightS);
1181                        String durationS = file.findChildContent("duration", "https://schema.org/");
1182                        if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1183                    }
1184
1185                    final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1186                    if (sources != null) {
1187                        final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1188                        if (ref != null) url = ref.getAttribute("uri");
1189                    }
1190                }
1191            }
1192        }
1193
1194        public FileParams(String ser) {
1195            final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1196            switch (parts.length) {
1197                case 1:
1198                    try {
1199                        this.size = Long.parseLong(parts[0]);
1200                    } catch (final NumberFormatException e) {
1201                        this.url = URL.tryParse(parts[0]);
1202                    }
1203                    break;
1204                case 5:
1205                    this.runtime = parseInt(parts[4]);
1206                case 4:
1207                    this.width = parseInt(parts[2]);
1208                    this.height = parseInt(parts[3]);
1209                case 2:
1210                    this.url = URL.tryParse(parts[0]);
1211                    this.size = Longs.tryParse(parts[1]);
1212                    break;
1213                case 3:
1214                    this.size = Longs.tryParse(parts[0]);
1215                    this.width = parseInt(parts[1]);
1216                    this.height = parseInt(parts[2]);
1217                    break;
1218            }
1219        }
1220
1221        public long getSize() {
1222            return size == null ? 0 : size;
1223        }
1224
1225        public String getName() {
1226            Element file = getFileElement();
1227            if (file == null) return null;
1228
1229            return file.findChildContent("name", file.getNamespace());
1230        }
1231
1232        public Element toSims() {
1233            if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1234            sims.setAttribute("type", "data");
1235            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1236            if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1237
1238            Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1239            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1240            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1241            if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1242
1243            file.removeChild(file.findChild("size", file.getNamespace()));
1244            if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1245
1246            file.removeChild(file.findChild("width", "https://schema.org/"));
1247            if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1248
1249            file.removeChild(file.findChild("height", "https://schema.org/"));
1250            if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1251
1252            file.removeChild(file.findChild("duration", "https://schema.org/"));
1253            if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1254
1255            if (url != null) {
1256                Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1257                if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1258
1259                Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1260                if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1261                source.setAttribute("type", "data");
1262                source.setAttribute("uri", url);
1263            }
1264
1265            return sims;
1266        }
1267
1268        protected Element getFileElement() {
1269            Element file = null;
1270            if (sims == null) return file;
1271
1272            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1273            if (mediaSharing == null) return file;
1274            file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1275            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1276            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1277            return file;
1278        }
1279
1280        public List<Cid> getCids() {
1281            List<Cid> cids = new ArrayList<>();
1282            Element file = getFileElement();
1283            if (file == null) return cids;
1284
1285            for (Element child : file.getChildren()) {
1286                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1287                    try {
1288                        cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1289                    } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1290                }
1291            }
1292
1293            cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1294
1295            return cids;
1296        }
1297
1298        public List<Element> getThumbnails() {
1299            List<Element> thumbs = new ArrayList<>();
1300            Element file = getFileElement();
1301            if (file == null) return thumbs;
1302
1303            for (Element child : file.getChildren()) {
1304                if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1305                    thumbs.add(child);
1306                }
1307            }
1308
1309            return thumbs;
1310        }
1311
1312        public String toString() {
1313            final StringBuilder builder = new StringBuilder();
1314            if (url != null) builder.append(url);
1315            if (size != null) builder.append('|').append(size.toString());
1316            if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1317            if (height > 0 || runtime > 0) builder.append('|').append(height);
1318            if (runtime > 0) builder.append('|').append(runtime);
1319            return builder.toString();
1320        }
1321
1322        public boolean equals(Object o) {
1323            if (!(o instanceof FileParams)) return false;
1324            if (url == null) return false;
1325
1326            return url.equals(((FileParams) o).url);
1327        }
1328
1329        public int hashCode() {
1330            return url == null ? super.hashCode() : url.hashCode();
1331        }
1332    }
1333
1334    public void setFingerprint(String fingerprint) {
1335        this.axolotlFingerprint = fingerprint;
1336    }
1337
1338    public String getFingerprint() {
1339        return axolotlFingerprint;
1340    }
1341
1342    public boolean isTrusted() {
1343        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1344        final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1345        return s != null && s.isTrusted();
1346    }
1347
1348    private int getPreviousEncryption() {
1349        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1350            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1351                continue;
1352            }
1353            return iterator.getEncryption();
1354        }
1355        return ENCRYPTION_NONE;
1356    }
1357
1358    private int getNextEncryption() {
1359        if (this.conversation instanceof Conversation) {
1360            Conversation conversation = (Conversation) this.conversation;
1361            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1362                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1363                    continue;
1364                }
1365                return iterator.getEncryption();
1366            }
1367            return conversation.getNextEncryption();
1368        } else {
1369            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1370        }
1371    }
1372
1373    public boolean isValidInSession() {
1374        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1375        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1376
1377        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1378                || futureEncryption == ENCRYPTION_NONE
1379                || pastEncryption != futureEncryption;
1380
1381        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1382    }
1383
1384    private static int getCleanedEncryption(int encryption) {
1385        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1386            return ENCRYPTION_PGP;
1387        }
1388        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1389            return ENCRYPTION_AXOLOTL;
1390        }
1391        return encryption;
1392    }
1393
1394    public static boolean configurePrivateMessage(final Message message) {
1395        return configurePrivateMessage(message, false);
1396    }
1397
1398    public static boolean configurePrivateFileMessage(final Message message) {
1399        return configurePrivateMessage(message, true);
1400    }
1401
1402    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1403        final Conversation conversation;
1404        if (message.conversation instanceof Conversation) {
1405            conversation = (Conversation) message.conversation;
1406        } else {
1407            return false;
1408        }
1409        if (conversation.getMode() == Conversation.MODE_MULTI) {
1410            final Jid nextCounterpart = conversation.getNextCounterpart();
1411            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1412        }
1413        return false;
1414    }
1415
1416    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1417        final Conversation conversation;
1418        if (message.conversation instanceof Conversation) {
1419            conversation = (Conversation) message.conversation;
1420        } else {
1421            return false;
1422        }
1423        return configurePrivateMessage(conversation, message, counterpart, false);
1424    }
1425
1426    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1427        if (counterpart == null) {
1428            return false;
1429        }
1430        message.setCounterpart(counterpart);
1431        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1432        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1433        return true;
1434    }
1435}