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