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