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