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).findOwnReactionsTo(replyId());
 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 Element getReactions() {
 422        if (this.payloads == null) return null;
 423
 424        for (Element el : this.payloads) {
 425            if (el.getName().equals("reactions") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
 426                return el;
 427            }
 428        }
 429
 430        return null;
 431    }
 432
 433    public String getConversationUuid() {
 434        return conversationUuid;
 435    }
 436
 437    public Conversational getConversation() {
 438        return this.conversation;
 439    }
 440
 441    public Jid getCounterpart() {
 442        return counterpart;
 443    }
 444
 445    public void setCounterpart(final Jid counterpart) {
 446        this.counterpart = counterpart;
 447    }
 448
 449    public Contact getContact() {
 450        if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
 451            if (this.trueCounterpart != null) {
 452                return this.conversation.getAccount().getRoster()
 453                           .getContact(this.trueCounterpart);
 454            }
 455
 456            return this.conversation.getContact();
 457        } else {
 458            if (this.trueCounterpart == null) {
 459                return null;
 460            } else {
 461                return this.conversation.getAccount().getRoster()
 462                        .getContactFromContactList(this.trueCounterpart);
 463            }
 464        }
 465    }
 466
 467    public String getQuoteableBody() {
 468        return this.body;
 469    }
 470
 471    public String getBody() {
 472        StringBuilder body = new StringBuilder(this.body);
 473
 474        List<Element> fallbacks = getFallbacks();
 475        List<Pair<Integer, Integer>> spans = new ArrayList<>();
 476        for (Element fallback : fallbacks) {
 477            for (Element span : fallback.getChildren()) {
 478                if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
 479                if (span.getAttribute("start") == null || span.getAttribute("end") == null) return "";
 480                spans.add(new Pair(parseInt(span.getAttribute("start")), parseInt(span.getAttribute("end"))));
 481            }
 482        }
 483        // Do them in reverse order so that span deletions don't affect the indexes of other spans
 484        spans.sort((x, y) -> y.first.compareTo(x.first));
 485        try {
 486            for (Pair<Integer, Integer> span : spans) {
 487                body.delete(span.first, span.second);
 488            }
 489        } catch (final StringIndexOutOfBoundsException e) { spans.clear(); }
 490
 491        if (spans.isEmpty() && getOob() != null) {
 492            return body.toString().replace(getOob().toString(), "");
 493        } else {
 494            return body.toString();
 495        }
 496    }
 497
 498    public synchronized void setBody(String body) {
 499        if (body == null) {
 500            throw new Error("You should not set the message body to null");
 501        }
 502        this.body = body;
 503        this.isGeoUri = null;
 504        this.isEmojisOnly = null;
 505        this.treatAsDownloadable = null;
 506    }
 507
 508    public synchronized void appendBody(String append) {
 509        this.body += append;
 510        this.isGeoUri = null;
 511        this.isEmojisOnly = null;
 512        this.treatAsDownloadable = null;
 513    }
 514
 515    public String getSubject() {
 516        return subject;
 517    }
 518
 519    public synchronized void setSubject(String subject) {
 520        this.subject = subject;
 521    }
 522
 523    public Element getThread() {
 524        if (this.payloads == null) return null;
 525
 526        for (Element el : this.payloads) {
 527            if (el.getName().equals("thread") && el.getNamespace().equals("jabber:client")) {
 528                return el;
 529            }
 530        }
 531
 532        return null;
 533    }
 534
 535    public void setThread(Element thread) {
 536        payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
 537        addPayload(thread);
 538    }
 539
 540    public void setMucUser(MucOptions.User user) {
 541        this.user = new WeakReference<>(user);
 542    }
 543
 544    public boolean sameMucUser(Message otherMessage) {
 545        final MucOptions.User thisUser = this.user == null ? null : this.user.get();
 546        final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
 547        return thisUser != null && thisUser == otherUser;
 548    }
 549
 550    public String getErrorMessage() {
 551        return errorMessage;
 552    }
 553
 554    public boolean setErrorMessage(String message) {
 555        boolean changed = (message != null && !message.equals(errorMessage))
 556                || (message == null && errorMessage != null);
 557        this.errorMessage = message;
 558        return changed;
 559    }
 560
 561    public long getTimeReceived() {
 562        return timeReceived;
 563    }
 564
 565    public long getTimeSent() {
 566        return timeSent;
 567    }
 568
 569    public int getEncryption() {
 570        return encryption;
 571    }
 572
 573    public void setEncryption(int encryption) {
 574        this.encryption = encryption;
 575    }
 576
 577    public int getStatus() {
 578        return status;
 579    }
 580
 581    public void setStatus(int status) {
 582        this.status = status;
 583    }
 584
 585    public String getRelativeFilePath() {
 586        return this.relativeFilePath;
 587    }
 588
 589    public void setRelativeFilePath(String path) {
 590        this.relativeFilePath = path;
 591    }
 592
 593    public String getRemoteMsgId() {
 594        return this.remoteMsgId;
 595    }
 596
 597    public void setRemoteMsgId(String id) {
 598        this.remoteMsgId = id;
 599    }
 600
 601    public String getServerMsgId() {
 602        return this.serverMsgId;
 603    }
 604
 605    public void setServerMsgId(String id) {
 606        this.serverMsgId = id;
 607    }
 608
 609    public boolean isRead() {
 610        return this.read;
 611    }
 612
 613    public boolean isDeleted() {
 614        return this.deleted;
 615    }
 616
 617    public Element getModerated() {
 618        if (this.payloads == null) return null;
 619
 620        for (Element el : this.payloads) {
 621            if (el.getName().equals("moderated") && el.getNamespace().equals("urn:xmpp:message-moderate:0")) {
 622                return el;
 623            }
 624        }
 625
 626        return null;
 627    }
 628
 629    public void setDeleted(boolean deleted) {
 630        this.deleted = deleted;
 631    }
 632
 633    public void markRead() {
 634        this.read = true;
 635    }
 636
 637    public void markUnread() {
 638        this.read = false;
 639    }
 640
 641    public void setTime(long time) {
 642        this.timeSent = time;
 643    }
 644
 645    public void setTimeReceived(long time) {
 646        this.timeReceived = time;
 647    }
 648
 649    public String getEncryptedBody() {
 650        return this.encryptedBody;
 651    }
 652
 653    public void setEncryptedBody(String body) {
 654        this.encryptedBody = body;
 655    }
 656
 657    public int getType() {
 658        return this.type;
 659    }
 660
 661    public void setType(int type) {
 662        this.type = type;
 663    }
 664
 665    public boolean isCarbon() {
 666        return carbon;
 667    }
 668
 669    public void setCarbon(boolean carbon) {
 670        this.carbon = carbon;
 671    }
 672
 673    public void putEdited(String edited, String serverMsgId) {
 674        final Edit edit = new Edit(edited, serverMsgId);
 675        if (this.edits.size() < 128 && !this.edits.contains(edit)) {
 676            this.edits.add(edit);
 677        }
 678    }
 679
 680    boolean remoteMsgIdMatchInEdit(String id) {
 681        for (Edit edit : this.edits) {
 682            if (id.equals(edit.getEditedId())) {
 683                return true;
 684            }
 685        }
 686        return false;
 687    }
 688
 689    public String getBodyLanguage() {
 690        return this.bodyLanguage;
 691    }
 692
 693    public void setBodyLanguage(String language) {
 694        this.bodyLanguage = language;
 695    }
 696
 697    public boolean edited() {
 698        return this.edits.size() > 0;
 699    }
 700
 701    public void setTrueCounterpart(Jid trueCounterpart) {
 702        this.trueCounterpart = trueCounterpart;
 703    }
 704
 705    public Jid getTrueCounterpart() {
 706        return this.trueCounterpart;
 707    }
 708
 709    public Transferable getTransferable() {
 710        return this.transferable;
 711    }
 712
 713    public synchronized void setTransferable(Transferable transferable) {
 714        this.transferable = transferable;
 715    }
 716
 717    public boolean addReadByMarker(ReadByMarker readByMarker) {
 718        if (readByMarker.getRealJid() != null) {
 719            if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
 720                return false;
 721            }
 722        } else if (readByMarker.getFullJid() != null) {
 723            if (readByMarker.getFullJid().equals(counterpart)) {
 724                return false;
 725            }
 726        }
 727        if (this.readByMarkers.add(readByMarker)) {
 728            if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
 729                Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
 730                while (iterator.hasNext()) {
 731                    ReadByMarker marker = iterator.next();
 732                    if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
 733                        iterator.remove();
 734                    }
 735                }
 736            }
 737            return true;
 738        } else {
 739            return false;
 740        }
 741    }
 742
 743    public Set<ReadByMarker> getReadByMarkers() {
 744        return ImmutableSet.copyOf(this.readByMarkers);
 745    }
 746
 747    boolean similar(Message message) {
 748        if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 749            return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 750        } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 751            return true;
 752        } else if (this.body == null || this.counterpart == null) {
 753            return false;
 754        } else {
 755            String body, otherBody;
 756            if (this.hasFileOnRemoteHost()) {
 757                body = getFileParams().url;
 758                otherBody = message.body == null ? null : message.body.trim();
 759            } else {
 760                body = this.body;
 761                otherBody = message.body;
 762            }
 763            final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
 764            if (message.getRemoteMsgId() != null) {
 765                final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
 766                if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
 767                    return true;
 768                }
 769                return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
 770                        && matchingCounterpart
 771                        && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
 772            } else {
 773                return this.remoteMsgId == null
 774                        && matchingCounterpart
 775                        && body.equals(otherBody)
 776                        && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
 777            }
 778        }
 779    }
 780
 781    public Message next() {
 782        if (this.conversation instanceof Conversation) {
 783            final Conversation conversation = (Conversation) this.conversation;
 784            synchronized (conversation.messages) {
 785                if (this.mNextMessage == null) {
 786                    int index = conversation.messages.indexOf(this);
 787                    if (index < 0 || index >= conversation.messages.size() - 1) {
 788                        this.mNextMessage = null;
 789                    } else {
 790                        this.mNextMessage = conversation.messages.get(index + 1);
 791                    }
 792                }
 793                return this.mNextMessage;
 794            }
 795        } else {
 796            throw new AssertionError("Calling next should be disabled for stubs");
 797        }
 798    }
 799
 800    public Message prev() {
 801        if (this.conversation instanceof Conversation) {
 802            final Conversation conversation = (Conversation) this.conversation;
 803            synchronized (conversation.messages) {
 804                if (this.mPreviousMessage == null) {
 805                    int index = conversation.messages.indexOf(this);
 806                    if (index <= 0 || index > conversation.messages.size()) {
 807                        this.mPreviousMessage = null;
 808                    } else {
 809                        this.mPreviousMessage = conversation.messages.get(index - 1);
 810                    }
 811                }
 812            }
 813            return this.mPreviousMessage;
 814        } else {
 815            throw new AssertionError("Calling prev should be disabled for stubs");
 816        }
 817    }
 818
 819    public boolean isLastCorrectableMessage() {
 820        Message next = next();
 821        while (next != null) {
 822            if (next.isEditable()) {
 823                return false;
 824            }
 825            next = next.next();
 826        }
 827        return isEditable();
 828    }
 829
 830    public boolean isEditable() {
 831        return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
 832    }
 833
 834    public boolean mergeable(final Message message) {
 835        return message != null &&
 836                (message.getType() == Message.TYPE_TEXT &&
 837                        this.getTransferable() == null &&
 838                        message.getTransferable() == null &&
 839                        message.getEncryption() != Message.ENCRYPTION_PGP &&
 840                        message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
 841                        this.getType() == message.getType() &&
 842                        this.getSubject() != null &&
 843                        isStatusMergeable(this.getStatus(), message.getStatus()) &&
 844                        isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
 845                        this.getCounterpart() != null &&
 846                        this.getCounterpart().equals(message.getCounterpart()) &&
 847                        this.edited() == message.edited() &&
 848                        (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 849                        this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 850                        !message.isGeoUri() &&
 851                        !this.isGeoUri() &&
 852                        !message.isOOb() &&
 853                        !this.isOOb() &&
 854                        !message.treatAsDownloadable() &&
 855                        !this.treatAsDownloadable() &&
 856                        !message.hasMeCommand() &&
 857                        !this.hasMeCommand() &&
 858                        !this.bodyIsOnlyEmojis() &&
 859                        !message.bodyIsOnlyEmojis() &&
 860                        ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 861                        UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 862                        this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 863                        !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 864                );
 865    }
 866
 867    private static boolean isStatusMergeable(int a, int b) {
 868        return a == b || (
 869                (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 870                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 871                        || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 872                        || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 873                        || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 874        );
 875    }
 876
 877    private static boolean isEncryptionMergeable(final int a, final int b) {
 878        return a == b
 879                && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
 880                        .contains(a);
 881    }
 882
 883    public void setCounterparts(List<MucOptions.User> counterparts) {
 884        this.counterparts = counterparts;
 885    }
 886
 887    public List<MucOptions.User> getCounterparts() {
 888        return this.counterparts;
 889    }
 890
 891    @Override
 892    public int getAvatarBackgroundColor() {
 893        if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 894            return Color.TRANSPARENT;
 895        } else {
 896            return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 897        }
 898    }
 899
 900    @Override
 901    public String getAvatarName() {
 902        return UIHelper.getMessageDisplayName(this);
 903    }
 904
 905    public boolean isOOb() {
 906        return oob || getFileParams().url != null;
 907    }
 908
 909    public static class MergeSeparator {
 910    }
 911
 912    public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
 913        final Element html = getHtml();
 914        if (html == null || Build.VERSION.SDK_INT < 24) {
 915            return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
 916        } else {
 917            SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
 918                MessageUtils.filterLtrRtl(html.toString()).trim(),
 919                Html.FROM_HTML_MODE_COMPACT,
 920                (source) -> {
 921                   try {
 922                       if (thumbnailer == null) return fallbackImg;
 923                       Cid cid = BobTransfer.cid(new URI(source));
 924                       if (cid == null) return fallbackImg;
 925                       Drawable thumbnail = thumbnailer.getThumbnail(cid);
 926                       if (thumbnail == null) return fallbackImg;
 927                       return thumbnail;
 928                   } catch (final URISyntaxException e) {
 929                       return fallbackImg;
 930                   }
 931                },
 932                (opening, tag, output, xmlReader) -> {}
 933            ));
 934
 935            // Make images clickable and long-clickable with BetterLinkMovementMethod
 936            ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
 937            for (ImageSpan span : imageSpans) {
 938                final int start = spannable.getSpanStart(span);
 939                final int end = spannable.getSpanEnd(span);
 940
 941                ClickableSpan click_span = new ClickableSpan() {
 942                    @Override
 943                    public void onClick(View widget) { }
 944                };
 945
 946                spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 947            }
 948
 949            // https://stackoverflow.com/a/10187511/8611
 950            int i = spannable.length();
 951            while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
 952            return (SpannableStringBuilder) spannable.subSequence(0, i+1);
 953        }
 954    }
 955
 956    public SpannableStringBuilder getMergedBody() {
 957        return getMergedBody(null, null);
 958    }
 959
 960    public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
 961        SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
 962        Message current = this;
 963        while (current.mergeable(current.next())) {
 964            current = current.next();
 965            if (current == null) {
 966                break;
 967            }
 968            body.append("\n\n");
 969            body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 970                    SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 971            body.append(current.getSpannableBody(thumbnailer, fallbackImg));
 972        }
 973        return body;
 974    }
 975
 976    public boolean hasMeCommand() {
 977        return this.body.trim().startsWith(ME_COMMAND);
 978    }
 979
 980    public int getMergedStatus() {
 981        int status = this.status;
 982        Message current = this;
 983        while (current.mergeable(current.next())) {
 984            current = current.next();
 985            if (current == null) {
 986                break;
 987            }
 988            status = current.status;
 989        }
 990        return status;
 991    }
 992
 993    public long getMergedTimeSent() {
 994        long time = this.timeSent;
 995        Message current = this;
 996        while (current.mergeable(current.next())) {
 997            current = current.next();
 998            if (current == null) {
 999                break;
1000            }
1001            time = current.timeSent;
1002        }
1003        return time;
1004    }
1005
1006    public boolean wasMergedIntoPrevious() {
1007        Message prev = this.prev();
1008        return prev != null && prev.mergeable(this);
1009    }
1010
1011    public boolean trusted() {
1012        Contact contact = this.getContact();
1013        return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
1014    }
1015
1016    public boolean fixCounterpart() {
1017        final Presences presences = conversation.getContact().getPresences();
1018        if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
1019            return true;
1020        } else if (presences.size() >= 1) {
1021            counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
1022            return true;
1023        } else {
1024            counterpart = null;
1025            return false;
1026        }
1027    }
1028
1029    public void setUuid(String uuid) {
1030        this.uuid = uuid;
1031    }
1032
1033    public String getEditedId() {
1034        if (edits.size() > 0) {
1035            return edits.get(edits.size() - 1).getEditedId();
1036        } else {
1037            throw new IllegalStateException("Attempting to store unedited message");
1038        }
1039    }
1040
1041    public String getEditedIdWireFormat() {
1042        if (edits.size() > 0) {
1043            return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
1044        } else {
1045            throw new IllegalStateException("Attempting to store unedited message");
1046        }
1047    }
1048
1049    public URI getOob() {
1050        final String url = getFileParams().url;
1051        try {
1052            return url == null ? null : new URI(url);
1053        } catch (final URISyntaxException e) {
1054            return null;
1055        }
1056    }
1057
1058    public void clearPayloads() {
1059        this.payloads.clear();
1060    }
1061
1062    public void addPayload(Element el) {
1063        if (el == null) return;
1064
1065        this.payloads.add(el);
1066    }
1067
1068    public List<Element> getPayloads() {
1069       return new ArrayList<>(this.payloads);
1070    }
1071
1072    public List<Element> getFallbacks() {
1073        List<Element> fallbacks = new ArrayList<>();
1074
1075        if (this.payloads == null) return fallbacks;
1076
1077        for (Element el : this.payloads) {
1078            if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
1079                final String fallbackFor = el.getAttribute("for");
1080                if (fallbackFor == null) continue;
1081                if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
1082                    fallbacks.add(el);
1083                }
1084            }
1085        }
1086
1087        return fallbacks;
1088    }
1089
1090    public Element getHtml() {
1091        if (this.payloads == null) return null;
1092
1093        for (Element el : this.payloads) {
1094            if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
1095                return el.getChildren().get(0);
1096            }
1097        }
1098
1099        return null;
1100   }
1101
1102    public List<Element> getCommands() {
1103        if (this.payloads == null) return null;
1104
1105        for (Element el : this.payloads) {
1106            if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
1107                return el.getChildren();
1108            }
1109        }
1110
1111        return null;
1112    }
1113
1114    public String getMimeType() {
1115        String extension;
1116        if (relativeFilePath != null) {
1117            extension = MimeUtils.extractRelevantExtension(relativeFilePath);
1118        } else {
1119            final String url = URL.tryParse(getOob() == null ? body.split("\n")[0] : getOob().toString());
1120            if (url == null) {
1121                return null;
1122            }
1123            extension = MimeUtils.extractRelevantExtension(url);
1124        }
1125        return MimeUtils.guessMimeTypeFromExtension(extension);
1126    }
1127
1128    public synchronized boolean treatAsDownloadable() {
1129        if (treatAsDownloadable == null) {
1130            treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb());
1131        }
1132        return treatAsDownloadable;
1133    }
1134
1135    public synchronized boolean bodyIsOnlyEmojis() {
1136        if (isEmojisOnly == null) {
1137            isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
1138        }
1139        return isEmojisOnly;
1140    }
1141
1142    public synchronized boolean isGeoUri() {
1143        if (isGeoUri == null) {
1144            isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
1145        }
1146        return isGeoUri;
1147    }
1148
1149    protected List<Element> getSims() {
1150        return payloads.stream().filter(el ->
1151            el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0") &&
1152            el.findChild("media-sharing", "urn:xmpp:sims:1") != null
1153        ).collect(Collectors.toList());
1154    }
1155
1156    public synchronized void resetFileParams() {
1157        this.fileParams = null;
1158    }
1159
1160    public synchronized void setFileParams(FileParams fileParams) {
1161        if (fileParams != null && this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
1162            fileParams.sims = this.fileParams.sims;
1163        }
1164        this.fileParams = fileParams;
1165        if (fileParams != null && getSims().isEmpty()) {
1166            addPayload(fileParams.toSims());
1167        }
1168    }
1169
1170    public synchronized FileParams getFileParams() {
1171        if (fileParams == null) {
1172            List<Element> sims = getSims();
1173            fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1174            if (this.transferable != null) {
1175                fileParams.size = this.transferable.getFileSize();
1176            }
1177        }
1178
1179        return fileParams;
1180    }
1181
1182    private static int parseInt(String value) {
1183        try {
1184            return Integer.parseInt(value);
1185        } catch (NumberFormatException e) {
1186            return 0;
1187        }
1188    }
1189
1190    public void untie() {
1191        this.mNextMessage = null;
1192        this.mPreviousMessage = null;
1193    }
1194
1195    public boolean isPrivateMessage() {
1196        return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1197    }
1198
1199    public boolean isFileOrImage() {
1200        return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1201    }
1202
1203
1204    public boolean isTypeText() {
1205        return type == TYPE_TEXT || type == TYPE_PRIVATE;
1206    }
1207
1208    public boolean hasFileOnRemoteHost() {
1209        return isFileOrImage() && getFileParams().url != null;
1210    }
1211
1212    public boolean needsUploading() {
1213        return isFileOrImage() && getFileParams().url == null;
1214    }
1215
1216    public static class FileParams {
1217        public String url;
1218        public Long size = null;
1219        public int width = 0;
1220        public int height = 0;
1221        public int runtime = 0;
1222        public Element sims = null;
1223
1224        public FileParams() { }
1225
1226        public FileParams(Element el) {
1227            if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1228                this.url = el.findChildContent("url", Namespace.OOB);
1229            }
1230            if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1231                sims = el;
1232                final String refUri = el.getAttribute("uri");
1233                if (refUri != null) url = refUri;
1234                final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1235                if (mediaSharing != null) {
1236                    Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1237                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1238                    if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1239                    if (file != null) {
1240                        String sizeS = file.findChildContent("size", file.getNamespace());
1241                        if (sizeS != null) size = new Long(sizeS);
1242                        String widthS = file.findChildContent("width", "https://schema.org/");
1243                        if (widthS != null) width = parseInt(widthS);
1244                        String heightS = file.findChildContent("height", "https://schema.org/");
1245                        if (heightS != null) height = parseInt(heightS);
1246                        String durationS = file.findChildContent("duration", "https://schema.org/");
1247                        if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1248                    }
1249
1250                    final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1251                    if (sources != null) {
1252                        final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1253                        if (ref != null) url = ref.getAttribute("uri");
1254                    }
1255                }
1256            }
1257        }
1258
1259        public FileParams(String ser) {
1260            final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1261            switch (parts.length) {
1262                case 1:
1263                    try {
1264                        this.size = Long.parseLong(parts[0]);
1265                    } catch (final NumberFormatException e) {
1266                        this.url = URL.tryParse(parts[0]);
1267                    }
1268                    break;
1269                case 5:
1270                    this.runtime = parseInt(parts[4]);
1271                case 4:
1272                    this.width = parseInt(parts[2]);
1273                    this.height = parseInt(parts[3]);
1274                case 2:
1275                    this.url = URL.tryParse(parts[0]);
1276                    this.size = Longs.tryParse(parts[1]);
1277                    break;
1278                case 3:
1279                    this.size = Longs.tryParse(parts[0]);
1280                    this.width = parseInt(parts[1]);
1281                    this.height = parseInt(parts[2]);
1282                    break;
1283            }
1284        }
1285
1286        public long getSize() {
1287            return size == null ? 0 : size;
1288        }
1289
1290        public String getName() {
1291            Element file = getFileElement();
1292            if (file == null) return null;
1293
1294            return file.findChildContent("name", file.getNamespace());
1295        }
1296
1297        public void setName(final String name) {
1298            if (sims == null) toSims();
1299            Element file = getFileElement();
1300
1301            for (Element child : file.getChildren()) {
1302                if (child.getName().equals("name") && child.getNamespace().equals(file.getNamespace())) {
1303                    file.removeChild(child);
1304                }
1305            }
1306
1307            if (name != null) {
1308                file.addChild("name", file.getNamespace()).setContent(name);
1309            }
1310        }
1311
1312        public Element toSims() {
1313            if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1314            sims.setAttribute("type", "data");
1315            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1316            if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1317
1318            Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1319            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1320            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1321            if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1322
1323            file.removeChild(file.findChild("size", file.getNamespace()));
1324            if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1325
1326            file.removeChild(file.findChild("width", "https://schema.org/"));
1327            if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1328
1329            file.removeChild(file.findChild("height", "https://schema.org/"));
1330            if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1331
1332            file.removeChild(file.findChild("duration", "https://schema.org/"));
1333            if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1334
1335            if (url != null) {
1336                Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1337                if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1338
1339                Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1340                if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1341                source.setAttribute("type", "data");
1342                source.setAttribute("uri", url);
1343            }
1344
1345            return sims;
1346        }
1347
1348        protected Element getFileElement() {
1349            Element file = null;
1350            if (sims == null) return file;
1351
1352            Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1353            if (mediaSharing == null) return file;
1354            file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1355            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1356            if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1357            return file;
1358        }
1359
1360        public void setCids(Iterable<Cid> cids) throws NoSuchAlgorithmException {
1361            if (sims == null) toSims();
1362            Element file = getFileElement();
1363
1364            for (Element child : file.getChildren()) {
1365                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1366                    file.removeChild(child);
1367                }
1368            }
1369
1370            for (Cid cid : cids) {
1371                file.addChild("hash", "urn:xmpp:hashes:2")
1372                    .setAttribute("algo", CryptoHelper.multihashAlgo(cid.getType()))
1373                    .setContent(Base64.encodeToString(cid.getHash(), Base64.NO_WRAP));
1374            }
1375        }
1376
1377        public List<Cid> getCids() {
1378            List<Cid> cids = new ArrayList<>();
1379            Element file = getFileElement();
1380            if (file == null) return cids;
1381
1382            for (Element child : file.getChildren()) {
1383                if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1384                    try {
1385                        cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1386                    } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1387                }
1388            }
1389
1390            cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1391
1392            return cids;
1393        }
1394
1395        public List<Element> getThumbnails() {
1396            List<Element> thumbs = new ArrayList<>();
1397            Element file = getFileElement();
1398            if (file == null) return thumbs;
1399
1400            for (Element child : file.getChildren()) {
1401                if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1402                    thumbs.add(child);
1403                }
1404            }
1405
1406            return thumbs;
1407        }
1408
1409        public String toString() {
1410            final StringBuilder builder = new StringBuilder();
1411            if (url != null) builder.append(url);
1412            if (size != null) builder.append('|').append(size.toString());
1413            if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1414            if (height > 0 || runtime > 0) builder.append('|').append(height);
1415            if (runtime > 0) builder.append('|').append(runtime);
1416            return builder.toString();
1417        }
1418
1419        public boolean equals(Object o) {
1420            if (!(o instanceof FileParams)) return false;
1421            if (url == null) return false;
1422
1423            return url.equals(((FileParams) o).url);
1424        }
1425
1426        public int hashCode() {
1427            return url == null ? super.hashCode() : url.hashCode();
1428        }
1429    }
1430
1431    public void setFingerprint(String fingerprint) {
1432        this.axolotlFingerprint = fingerprint;
1433    }
1434
1435    public String getFingerprint() {
1436        return axolotlFingerprint;
1437    }
1438
1439    public boolean isTrusted() {
1440        final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1441        final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1442        return s != null && s.isTrusted();
1443    }
1444
1445    private int getPreviousEncryption() {
1446        for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1447            if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1448                continue;
1449            }
1450            return iterator.getEncryption();
1451        }
1452        return ENCRYPTION_NONE;
1453    }
1454
1455    private int getNextEncryption() {
1456        if (this.conversation instanceof Conversation) {
1457            Conversation conversation = (Conversation) this.conversation;
1458            for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1459                if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1460                    continue;
1461                }
1462                return iterator.getEncryption();
1463            }
1464            return conversation.getNextEncryption();
1465        } else {
1466            throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1467        }
1468    }
1469
1470    public boolean isValidInSession() {
1471        int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1472        int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1473
1474        boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1475                || futureEncryption == ENCRYPTION_NONE
1476                || pastEncryption != futureEncryption;
1477
1478        return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1479    }
1480
1481    private static int getCleanedEncryption(int encryption) {
1482        if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1483            return ENCRYPTION_PGP;
1484        }
1485        if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1486            return ENCRYPTION_AXOLOTL;
1487        }
1488        return encryption;
1489    }
1490
1491    public static boolean configurePrivateMessage(final Message message) {
1492        return configurePrivateMessage(message, false);
1493    }
1494
1495    public static boolean configurePrivateFileMessage(final Message message) {
1496        return configurePrivateMessage(message, true);
1497    }
1498
1499    private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1500        final Conversation conversation;
1501        if (message.conversation instanceof Conversation) {
1502            conversation = (Conversation) message.conversation;
1503        } else {
1504            return false;
1505        }
1506        if (conversation.getMode() == Conversation.MODE_MULTI) {
1507            final Jid nextCounterpart = conversation.getNextCounterpart();
1508            return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1509        }
1510        return false;
1511    }
1512
1513    public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1514        final Conversation conversation;
1515        if (message.conversation instanceof Conversation) {
1516            conversation = (Conversation) message.conversation;
1517        } else {
1518            return false;
1519        }
1520        return configurePrivateMessage(conversation, message, counterpart, false);
1521    }
1522
1523    private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1524        if (counterpart == null) {
1525            return false;
1526        }
1527        message.setCounterpart(counterpart);
1528        message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1529        message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1530        return true;
1531    }
1532}