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        if (fileParams != null && getSims().isEmpty()) {
1104            addPayload(fileParams.toSims());
1105        }
1106    }
1107
1108    public synchronized FileParams getFileParams() {
1109        if (fileParams == null) {
1110            List<Element> sims = getSims();
1111            fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1112            if (this.transferable != null) {
1113                fileParams.size = this.transferable.getFileSize();
1114            }
1115        }
1116
1117        return fileParams;
1118    }
1119
1120    private static int parseInt(String value) {
1121        try {
1122            return Integer.parseInt(value);
1123        } catch (NumberFormatException e) {
1124            return 0;
1125        }
1126    }
1127
1128    public void untie() {
1129        this.mNextMessage = null;
1130        this.mPreviousMessage = null;
1131    }
1132
1133    public boolean isPrivateMessage() {
1134        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1135    }
1136
1137    public boolean isFileOrImage() {
1138        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1139    }
1140
1141
1142    public boolean isTypeText() {
1143        return type == TYPE_TEXT || type == TYPE_PRIVATE;
1144    }
1145
1146    public boolean hasFileOnRemoteHost() {
1147        return isFileOrImage() && getFileParams().url != null;
1148    }
1149
1150    public boolean needsUploading() {
1151        return isFileOrImage() && getFileParams().url == null;
1152    }
1153
1154    public static class FileParams {
1155        public String url;
1156        public Long size = null;
1157        public int width = 0;
1158        public int height = 0;
1159        public int runtime = 0;
1160        public Element sims = null;
1161
1162        public FileParams() { }
1163
1164        public FileParams(Element el) {
1165            if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1166                this.url = el.findChildContent("url", Namespace.OOB);
1167            }
1168            if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1169                sims = el;
1170                final String refUri = el.getAttribute("uri");
1171                if (refUri != null) url = refUri;
1172                final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1173                if (mediaSharing != null) {
1174                    Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1175                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1176                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1177                    if (file != null) {
1178                        String sizeS = file.findChildContent("size", file.getNamespace());
1179                        if (sizeS != null) size = new Long(sizeS);
1180                        String widthS = file.findChildContent("width", "https://schema.org/");
1181                        if (widthS != null) width = parseInt(widthS);
1182                        String heightS = file.findChildContent("height", "https://schema.org/");
1183                        if (heightS != null) height = parseInt(heightS);
1184                        String durationS = file.findChildContent("duration", "https://schema.org/");
1185                        if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1186                    }
1187
1188                    final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1189                    if (sources != null) {
1190                        final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1191                        if (ref != null) url = ref.getAttribute("uri");
1192                    }
1193                }
1194            }
1195        }
1196
1197        public FileParams(String ser) {
1198            final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1199            switch (parts.length) {
1200                case 1:
1201                    try {
1202                        this.size = Long.parseLong(parts[0]);
1203                    } catch (final NumberFormatException e) {
1204                        this.url = URL.tryParse(parts[0]);
1205                    }
1206                    break;
1207                case 5:
1208                    this.runtime = parseInt(parts[4]);
1209                case 4:
1210                    this.width = parseInt(parts[2]);
1211                    this.height = parseInt(parts[3]);
1212                case 2:
1213                    this.url = URL.tryParse(parts[0]);
1214                    this.size = Longs.tryParse(parts[1]);
1215                    break;
1216                case 3:
1217                    this.size = Longs.tryParse(parts[0]);
1218                    this.width = parseInt(parts[1]);
1219                    this.height = parseInt(parts[2]);
1220                    break;
1221            }
1222        }
1223
1224        public long getSize() {
1225            return size == null ? 0 : size;
1226        }
1227
1228        public String getName() {
1229            Element file = getFileElement();
1230            if (file == null) return null;
1231
1232            return file.findChildContent("name", file.getNamespace());
1233        }
1234
1235        public void setName(final String name) {
1236            if (sims == null) toSims();
1237            Element file = getFileElement();
1238
1239            for (Element child : file.getChildren()) {
1240                if (child.getName().equals("name") && child.getNamespace().equals(file.getNamespace())) {
1241                    file.removeChild(child);
1242                }
1243            }
1244
1245            if (name != null) {
1246                file.addChild("name", file.getNamespace()).setContent(name);
1247            }
1248        }
1249
1250        public Element toSims() {
1251            if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1252            sims.setAttribute("type", "data");
1253            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1254            if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1255
1256            Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1257            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1258            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1259            if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1260
1261            file.removeChild(file.findChild("size", file.getNamespace()));
1262            if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1263
1264            file.removeChild(file.findChild("width", "https://schema.org/"));
1265            if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1266
1267            file.removeChild(file.findChild("height", "https://schema.org/"));
1268            if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1269
1270            file.removeChild(file.findChild("duration", "https://schema.org/"));
1271            if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1272
1273            if (url != null) {
1274                Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1275                if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1276
1277                Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1278                if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1279                source.setAttribute("type", "data");
1280                source.setAttribute("uri", url);
1281            }
1282
1283            return sims;
1284        }
1285
1286        protected Element getFileElement() {
1287            Element file = null;
1288            if (sims == null) return file;
1289
1290            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1291            if (mediaSharing == null) return file;
1292            file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1293            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1294            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1295            return file;
1296        }
1297
1298        public void setCids(Iterable<Cid> cids) throws NoSuchAlgorithmException {
1299            if (sims == null) toSims();
1300            Element file = getFileElement();
1301
1302            for (Element child : file.getChildren()) {
1303                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1304                    file.removeChild(child);
1305                }
1306            }
1307
1308            for (Cid cid : cids) {
1309                file.addChild("hash", "urn:xmpp:hashes:2")
1310                    .setAttribute("algo", CryptoHelper.multihashAlgo(cid.getType()))
1311                    .setContent(Base64.encodeToString(cid.getHash(), Base64.NO_WRAP));
1312            }
1313        }
1314
1315        public List<Cid> getCids() {
1316            List<Cid> cids = new ArrayList<>();
1317            Element file = getFileElement();
1318            if (file == null) return cids;
1319
1320            for (Element child : file.getChildren()) {
1321                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1322                    try {
1323                        cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1324                    } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1325                }
1326            }
1327
1328            cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1329
1330            return cids;
1331        }
1332
1333        public List<Element> getThumbnails() {
1334            List<Element> thumbs = new ArrayList<>();
1335            Element file = getFileElement();
1336            if (file == null) return thumbs;
1337
1338            for (Element child : file.getChildren()) {
1339                if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1340                    thumbs.add(child);
1341                }
1342            }
1343
1344            return thumbs;
1345        }
1346
1347        public String toString() {
1348            final StringBuilder builder = new StringBuilder();
1349            if (url != null) builder.append(url);
1350            if (size != null) builder.append('|').append(size.toString());
1351            if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1352            if (height > 0 || runtime > 0) builder.append('|').append(height);
1353            if (runtime > 0) builder.append('|').append(runtime);
1354            return builder.toString();
1355        }
1356
1357        public boolean equals(Object o) {
1358            if (!(o instanceof FileParams)) return false;
1359            if (url == null) return false;
1360
1361            return url.equals(((FileParams) o).url);
1362        }
1363
1364        public int hashCode() {
1365            return url == null ? super.hashCode() : url.hashCode();
1366        }
1367    }
1368
1369    public void setFingerprint(String fingerprint) {
1370        this.axolotlFingerprint = fingerprint;
1371    }
1372
1373    public String getFingerprint() {
1374        return axolotlFingerprint;
1375    }
1376
1377    public boolean isTrusted() {
1378        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1379        final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1380        return s != null && s.isTrusted();
1381    }
1382
1383    private int getPreviousEncryption() {
1384        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1385            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1386                continue;
1387            }
1388            return iterator.getEncryption();
1389        }
1390        return ENCRYPTION_NONE;
1391    }
1392
1393    private int getNextEncryption() {
1394        if (this.conversation instanceof Conversation) {
1395            Conversation conversation = (Conversation) this.conversation;
1396            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1397                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1398                    continue;
1399                }
1400                return iterator.getEncryption();
1401            }
1402            return conversation.getNextEncryption();
1403        } else {
1404            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1405        }
1406    }
1407
1408    public boolean isValidInSession() {
1409        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1410        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1411
1412        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1413                || futureEncryption == ENCRYPTION_NONE
1414                || pastEncryption != futureEncryption;
1415
1416        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1417    }
1418
1419    private static int getCleanedEncryption(int encryption) {
1420        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1421            return ENCRYPTION_PGP;
1422        }
1423        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1424            return ENCRYPTION_AXOLOTL;
1425        }
1426        return encryption;
1427    }
1428
1429    public static boolean configurePrivateMessage(final Message message) {
1430        return configurePrivateMessage(message, false);
1431    }
1432
1433    public static boolean configurePrivateFileMessage(final Message message) {
1434        return configurePrivateMessage(message, true);
1435    }
1436
1437    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1438        final Conversation conversation;
1439        if (message.conversation instanceof Conversation) {
1440            conversation = (Conversation) message.conversation;
1441        } else {
1442            return false;
1443        }
1444        if (conversation.getMode() == Conversation.MODE_MULTI) {
1445            final Jid nextCounterpart = conversation.getNextCounterpart();
1446            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1447        }
1448        return false;
1449    }
1450
1451    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1452        final Conversation conversation;
1453        if (message.conversation instanceof Conversation) {
1454            conversation = (Conversation) message.conversation;
1455        } else {
1456            return false;
1457        }
1458        return configurePrivateMessage(conversation, message, counterpart, false);
1459    }
1460
1461    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1462        if (counterpart == null) {
1463            return false;
1464        }
1465        message.setCounterpart(counterpart);
1466        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1467        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1468        return true;
1469    }
1470}