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