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