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