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