Conversation.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.support.annotation.NonNull;
   6import android.support.annotation.Nullable;
   7import android.text.TextUtils;
   8
   9import com.google.common.collect.ComparisonChain;
  10import com.google.common.collect.Lists;
  11
  12import org.json.JSONArray;
  13import org.json.JSONException;
  14import org.json.JSONObject;
  15
  16import java.util.ArrayList;
  17import java.util.Collections;
  18import java.util.Iterator;
  19import java.util.List;
  20import java.util.ListIterator;
  21import java.util.concurrent.atomic.AtomicBoolean;
  22
  23import eu.siacs.conversations.Config;
  24import eu.siacs.conversations.crypto.OmemoSetting;
  25import eu.siacs.conversations.crypto.PgpDecryptionService;
  26import eu.siacs.conversations.persistance.DatabaseBackend;
  27import eu.siacs.conversations.services.AvatarService;
  28import eu.siacs.conversations.services.QuickConversationsService;
  29import eu.siacs.conversations.utils.JidHelper;
  30import eu.siacs.conversations.utils.UIHelper;
  31import eu.siacs.conversations.xmpp.chatstate.ChatState;
  32import eu.siacs.conversations.xmpp.mam.MamReference;
  33import eu.siacs.conversations.xmpp.Jid;
  34
  35import static eu.siacs.conversations.entities.Bookmark.printableValue;
  36
  37
  38public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
  39    public static final String TABLENAME = "conversations";
  40
  41    public static final int STATUS_AVAILABLE = 0;
  42    public static final int STATUS_ARCHIVED = 1;
  43
  44    public static final String NAME = "name";
  45    public static final String ACCOUNT = "accountUuid";
  46    public static final String CONTACT = "contactUuid";
  47    public static final String CONTACTJID = "contactJid";
  48    public static final String STATUS = "status";
  49    public static final String CREATED = "created";
  50    public static final String MODE = "mode";
  51    public static final String ATTRIBUTES = "attributes";
  52
  53    public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
  54    public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
  55    public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
  56    public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
  57    public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
  58    static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
  59    static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
  60    static final String ATTRIBUTE_MODERATED = "moderated";
  61    static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
  62    private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
  63    private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
  64    private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
  65    private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
  66    private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
  67    protected final ArrayList<Message> messages = new ArrayList<>();
  68    public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
  69    protected Account account = null;
  70    private String draftMessage;
  71    private String name;
  72    private String contactUuid;
  73    private String accountUuid;
  74    private Jid contactJid;
  75    private int status;
  76    private long created;
  77    private int mode;
  78    private JSONObject attributes;
  79    private Jid nextCounterpart;
  80    private transient MucOptions mucOptions = null;
  81    private boolean messagesLeftOnServer = true;
  82    private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
  83    private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
  84    private String mFirstMamReference = null;
  85
  86    public Conversation(final String name, final Account account, final Jid contactJid,
  87                        final int mode) {
  88        this(java.util.UUID.randomUUID().toString(), name, null, account
  89                        .getUuid(), contactJid, System.currentTimeMillis(),
  90                STATUS_AVAILABLE, mode, "");
  91        this.account = account;
  92    }
  93
  94    public Conversation(final String uuid, final String name, final String contactUuid,
  95                        final String accountUuid, final Jid contactJid, final long created, final int status,
  96                        final int mode, final String attributes) {
  97        this.uuid = uuid;
  98        this.name = name;
  99        this.contactUuid = contactUuid;
 100        this.accountUuid = accountUuid;
 101        this.contactJid = contactJid;
 102        this.created = created;
 103        this.status = status;
 104        this.mode = mode;
 105        try {
 106            this.attributes = new JSONObject(attributes == null ? "" : attributes);
 107        } catch (JSONException e) {
 108            this.attributes = new JSONObject();
 109        }
 110    }
 111
 112    public static Conversation fromCursor(Cursor cursor) {
 113        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 114                cursor.getString(cursor.getColumnIndex(NAME)),
 115                cursor.getString(cursor.getColumnIndex(CONTACT)),
 116                cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 117                JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
 118                cursor.getLong(cursor.getColumnIndex(CREATED)),
 119                cursor.getInt(cursor.getColumnIndex(STATUS)),
 120                cursor.getInt(cursor.getColumnIndex(MODE)),
 121                cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 122    }
 123
 124    public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
 125        for (int i = messages.size() - 1; i >= 0; --i) {
 126            final Message message = messages.get(i);
 127            if (message.getStatus() <= Message.STATUS_RECEIVED
 128                    && (message.markable || isPrivateAndNonAnonymousMuc)
 129                    && !message.isPrivateMessage()) {
 130                return message;
 131            }
 132        }
 133        return null;
 134    }
 135
 136    private static boolean suitableForOmemoByDefault(final Conversation conversation) {
 137        if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
 138            return false;
 139        }
 140        if (conversation.getContact().isOwnServer()) {
 141            return false;
 142        }
 143        final String contact = conversation.getJid().getDomain().toEscapedString();
 144        final String account = conversation.getAccount().getServer();
 145        if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
 146            return false;
 147        }
 148        return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
 149    }
 150
 151    public boolean hasMessagesLeftOnServer() {
 152        return messagesLeftOnServer;
 153    }
 154
 155    public void setHasMessagesLeftOnServer(boolean value) {
 156        this.messagesLeftOnServer = value;
 157    }
 158
 159    public Message getFirstUnreadMessage() {
 160        Message first = null;
 161        synchronized (this.messages) {
 162            for (int i = messages.size() - 1; i >= 0; --i) {
 163                if (messages.get(i).isRead()) {
 164                    return first;
 165                } else {
 166                    first = messages.get(i);
 167                }
 168            }
 169        }
 170        return first;
 171    }
 172
 173    public String findMostRecentRemoteDisplayableId() {
 174        final boolean multi = mode == Conversation.MODE_MULTI;
 175        synchronized (this.messages) {
 176            for (final Message message : Lists.reverse(this.messages)) {
 177                if (message.getStatus() == Message.STATUS_RECEIVED) {
 178                    final String serverMsgId = message.getServerMsgId();
 179                    if (serverMsgId != null && multi) {
 180                        return serverMsgId;
 181                    }
 182                    return message.getRemoteMsgId();
 183                }
 184            }
 185        }
 186        return null;
 187    }
 188
 189    public int countFailedDeliveries() {
 190        int count = 0;
 191        synchronized (this.messages) {
 192            for(final Message message : this.messages) {
 193                if (message.getStatus() == Message.STATUS_SEND_FAILED) {
 194                    ++count;
 195                }
 196            }
 197        }
 198        return count;
 199    }
 200
 201    public Message getLastEditableMessage() {
 202        synchronized (this.messages) {
 203            for (final Message message : Lists.reverse(this.messages)) {
 204                if (message.isEditable()) {
 205                    if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
 206                        return null;
 207                    }
 208                    return message;
 209                }
 210            }
 211        }
 212        return null;
 213    }
 214
 215
 216    public Message findUnsentMessageWithUuid(String uuid) {
 217        synchronized (this.messages) {
 218            for (final Message message : this.messages) {
 219                final int s = message.getStatus();
 220                if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 221                    return message;
 222                }
 223            }
 224        }
 225        return null;
 226    }
 227
 228    public void findWaitingMessages(OnMessageFound onMessageFound) {
 229        final ArrayList<Message> results = new ArrayList<>();
 230        synchronized (this.messages) {
 231            for (Message message : this.messages) {
 232                if (message.getStatus() == Message.STATUS_WAITING) {
 233                    results.add(message);
 234                }
 235            }
 236        }
 237        for (Message result : results) {
 238            onMessageFound.onMessageFound(result);
 239        }
 240    }
 241
 242    public void findUnreadMessages(OnMessageFound onMessageFound) {
 243        final ArrayList<Message> results = new ArrayList<>();
 244        synchronized (this.messages) {
 245            for (final Message message : this.messages) {
 246                if (message.isRead() || message.getType() == Message.TYPE_RTP_SESSION) {
 247                    continue;
 248                }
 249                results.add(message);
 250            }
 251        }
 252        for (final Message result : results) {
 253            onMessageFound.onMessageFound(result);
 254        }
 255    }
 256
 257    public Message findMessageWithFileAndUuid(final String uuid) {
 258        synchronized (this.messages) {
 259            for (final Message message : this.messages) {
 260                if (message.getUuid().equals(uuid)
 261                        && message.getEncryption() != Message.ENCRYPTION_PGP
 262                        && (message.isFileOrImage() || message.treatAsDownloadable())) {
 263                    return message;
 264                }
 265            }
 266        }
 267        return null;
 268    }
 269
 270    public boolean markAsDeleted(final List<String> uuids) {
 271        boolean deleted = false;
 272        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 273        synchronized (this.messages) {
 274            for (Message message : this.messages) {
 275                if (uuids.contains(message.getUuid())) {
 276                    message.setDeleted(true);
 277                    deleted = true;
 278                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 279                        pgpDecryptionService.discard(message);
 280                    }
 281                }
 282            }
 283        }
 284        return deleted;
 285    }
 286
 287    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
 288        boolean changed = false;
 289        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 290        synchronized (this.messages) {
 291            for (Message message : this.messages) {
 292                for (final DatabaseBackend.FilePathInfo file : files)
 293                    if (file.uuid.toString().equals(message.getUuid())) {
 294                        message.setDeleted(file.deleted);
 295                        changed = true;
 296                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 297                            pgpDecryptionService.discard(message);
 298                        }
 299                    }
 300            }
 301        }
 302        return changed;
 303    }
 304
 305    public void clearMessages() {
 306        synchronized (this.messages) {
 307            this.messages.clear();
 308        }
 309    }
 310
 311    public boolean setIncomingChatState(ChatState state) {
 312        if (this.mIncomingChatState == state) {
 313            return false;
 314        }
 315        this.mIncomingChatState = state;
 316        return true;
 317    }
 318
 319    public ChatState getIncomingChatState() {
 320        return this.mIncomingChatState;
 321    }
 322
 323    public boolean setOutgoingChatState(ChatState state) {
 324        if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
 325            if (this.mOutgoingChatState != state) {
 326                this.mOutgoingChatState = state;
 327                return true;
 328            }
 329        }
 330        return false;
 331    }
 332
 333    public ChatState getOutgoingChatState() {
 334        return this.mOutgoingChatState;
 335    }
 336
 337    public void trim() {
 338        synchronized (this.messages) {
 339            final int size = messages.size();
 340            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 341            if (size > maxsize) {
 342                List<Message> discards = this.messages.subList(0, size - maxsize);
 343                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 344                if (pgpDecryptionService != null) {
 345                    pgpDecryptionService.discard(discards);
 346                }
 347                discards.clear();
 348                untieMessages();
 349            }
 350        }
 351    }
 352
 353    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 354        final ArrayList<Message> results = new ArrayList<>();
 355        synchronized (this.messages) {
 356            for (Message message : this.messages) {
 357                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
 358                    results.add(message);
 359                }
 360            }
 361        }
 362        for (Message result : results) {
 363            onMessageFound.onMessageFound(result);
 364        }
 365    }
 366
 367    public Message findSentMessageWithUuidOrRemoteId(String id) {
 368        synchronized (this.messages) {
 369            for (Message message : this.messages) {
 370                if (id.equals(message.getUuid())
 371                        || (message.getStatus() >= Message.STATUS_SEND
 372                        && id.equals(message.getRemoteMsgId()))) {
 373                    return message;
 374                }
 375            }
 376        }
 377        return null;
 378    }
 379
 380    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
 381        synchronized (this.messages) {
 382            for (int i = this.messages.size() - 1; i >= 0; --i) {
 383                final Message message = messages.get(i);
 384                final Jid mcp = message.getCounterpart();
 385                if (mcp == null) {
 386                    continue;
 387                }
 388                if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
 389                        && (carbon == message.isCarbon() || received)) {
 390                    final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
 391                    if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
 392                        return message;
 393                    } else {
 394                        return null;
 395                    }
 396                }
 397            }
 398        }
 399        return null;
 400    }
 401
 402    public Message findSentMessageWithUuid(String id) {
 403        synchronized (this.messages) {
 404            for (Message message : this.messages) {
 405                if (id.equals(message.getUuid())) {
 406                    return message;
 407                }
 408            }
 409        }
 410        return null;
 411    }
 412
 413    public Message findMessageWithRemoteId(String id, Jid counterpart) {
 414        synchronized (this.messages) {
 415            for (Message message : this.messages) {
 416                if (counterpart.equals(message.getCounterpart())
 417                        && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
 418                    return message;
 419                }
 420            }
 421        }
 422        return null;
 423    }
 424
 425    public Message findMessageWithServerMsgId(String id) {
 426        synchronized (this.messages) {
 427            for (Message message : this.messages) {
 428                if (id != null && id.equals(message.getServerMsgId())) {
 429                    return message;
 430                }
 431            }
 432        }
 433        return null;
 434    }
 435
 436    public boolean hasMessageWithCounterpart(Jid counterpart) {
 437        synchronized (this.messages) {
 438            for (Message message : this.messages) {
 439                if (counterpart.equals(message.getCounterpart())) {
 440                    return true;
 441                }
 442            }
 443        }
 444        return false;
 445    }
 446
 447    public void populateWithMessages(final List<Message> messages) {
 448        synchronized (this.messages) {
 449            messages.clear();
 450            messages.addAll(this.messages);
 451        }
 452        for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
 453            if (iterator.next().wasMergedIntoPrevious()) {
 454                iterator.remove();
 455            }
 456        }
 457    }
 458
 459    @Override
 460    public boolean isBlocked() {
 461        return getContact().isBlocked();
 462    }
 463
 464    @Override
 465    public boolean isDomainBlocked() {
 466        return getContact().isDomainBlocked();
 467    }
 468
 469    @Override
 470    public Jid getBlockedJid() {
 471        return getContact().getBlockedJid();
 472    }
 473
 474    public int countMessages() {
 475        synchronized (this.messages) {
 476            return this.messages.size();
 477        }
 478    }
 479
 480    public String getFirstMamReference() {
 481        return this.mFirstMamReference;
 482    }
 483
 484    public void setFirstMamReference(String reference) {
 485        this.mFirstMamReference = reference;
 486    }
 487
 488    public void setLastClearHistory(long time, String reference) {
 489        if (reference != null) {
 490            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
 491        } else {
 492            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
 493        }
 494    }
 495
 496    public MamReference getLastClearHistory() {
 497        return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
 498    }
 499
 500    public List<Jid> getAcceptedCryptoTargets() {
 501        if (mode == MODE_SINGLE) {
 502            return Collections.singletonList(getJid().asBareJid());
 503        } else {
 504            return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 505        }
 506    }
 507
 508    public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 509        setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 510    }
 511
 512    public boolean setCorrectingMessage(Message correctingMessage) {
 513        setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
 514        return correctingMessage == null && draftMessage != null;
 515    }
 516
 517    public Message getCorrectingMessage() {
 518        final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
 519        return uuid == null ? null : findSentMessageWithUuid(uuid);
 520    }
 521
 522    public boolean withSelf() {
 523        return getContact().isSelf();
 524    }
 525
 526    @Override
 527    public int compareTo(@NonNull Conversation another) {
 528        return ComparisonChain.start()
 529                .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
 530                .compare(another.getSortableTime(), getSortableTime())
 531                .result();
 532    }
 533
 534    private long getSortableTime() {
 535        Draft draft = getDraft();
 536        long messageTime = getLatestMessage().getTimeSent();
 537        if (draft == null) {
 538            return messageTime;
 539        } else {
 540            return Math.max(messageTime, draft.getTimestamp());
 541        }
 542    }
 543
 544    public String getDraftMessage() {
 545        return draftMessage;
 546    }
 547
 548    public void setDraftMessage(String draftMessage) {
 549        this.draftMessage = draftMessage;
 550    }
 551
 552    public boolean isRead() {
 553        return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
 554    }
 555
 556    public List<Message> markRead(String upToUuid) {
 557        final List<Message> unread = new ArrayList<>();
 558        synchronized (this.messages) {
 559            for (Message message : this.messages) {
 560                if (!message.isRead()) {
 561                    message.markRead();
 562                    unread.add(message);
 563                }
 564                if (message.getUuid().equals(upToUuid)) {
 565                    return unread;
 566                }
 567            }
 568        }
 569        return unread;
 570    }
 571
 572    public Message getLatestMessage() {
 573        synchronized (this.messages) {
 574            if (this.messages.size() == 0) {
 575                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 576                message.setType(Message.TYPE_STATUS);
 577                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 578                return message;
 579            } else {
 580                return this.messages.get(this.messages.size() - 1);
 581            }
 582        }
 583    }
 584
 585    public @NonNull
 586    CharSequence getName() {
 587        if (getMode() == MODE_MULTI) {
 588            final String roomName = getMucOptions().getName();
 589            final String subject = getMucOptions().getSubject();
 590            final Bookmark bookmark = getBookmark();
 591            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 592            if (printableValue(roomName)) {
 593                return roomName;
 594            } else if (printableValue(subject)) {
 595                return subject;
 596            } else if (printableValue(bookmarkName, false)) {
 597                return bookmarkName;
 598            } else {
 599                final String generatedName = getMucOptions().createNameFromParticipants();
 600                if (printableValue(generatedName)) {
 601                    return generatedName;
 602                } else {
 603                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 604                }
 605            }
 606        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 607            return contactJid;
 608        } else {
 609            return this.getContact().getDisplayName();
 610        }
 611    }
 612
 613    public String getAccountUuid() {
 614        return this.accountUuid;
 615    }
 616
 617    public Account getAccount() {
 618        return this.account;
 619    }
 620
 621    public void setAccount(final Account account) {
 622        this.account = account;
 623    }
 624
 625    public Contact getContact() {
 626        return this.account.getRoster().getContact(this.contactJid);
 627    }
 628
 629    @Override
 630    public Jid getJid() {
 631        return this.contactJid;
 632    }
 633
 634    public int getStatus() {
 635        return this.status;
 636    }
 637
 638    public void setStatus(int status) {
 639        this.status = status;
 640    }
 641
 642    public long getCreated() {
 643        return this.created;
 644    }
 645
 646    public ContentValues getContentValues() {
 647        ContentValues values = new ContentValues();
 648        values.put(UUID, uuid);
 649        values.put(NAME, name);
 650        values.put(CONTACT, contactUuid);
 651        values.put(ACCOUNT, accountUuid);
 652        values.put(CONTACTJID, contactJid.toString());
 653        values.put(CREATED, created);
 654        values.put(STATUS, status);
 655        values.put(MODE, mode);
 656        synchronized (this.attributes) {
 657            values.put(ATTRIBUTES, attributes.toString());
 658        }
 659        return values;
 660    }
 661
 662    public int getMode() {
 663        return this.mode;
 664    }
 665
 666    public void setMode(int mode) {
 667        this.mode = mode;
 668    }
 669
 670    /**
 671     * short for is Private and Non-anonymous
 672     */
 673    public boolean isSingleOrPrivateAndNonAnonymous() {
 674        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 675    }
 676
 677    public boolean isPrivateAndNonAnonymous() {
 678        return getMucOptions().isPrivateAndNonAnonymous();
 679    }
 680
 681    public synchronized MucOptions getMucOptions() {
 682        if (this.mucOptions == null) {
 683            this.mucOptions = new MucOptions(this);
 684        }
 685        return this.mucOptions;
 686    }
 687
 688    public void resetMucOptions() {
 689        this.mucOptions = null;
 690    }
 691
 692    public void setContactJid(final Jid jid) {
 693        this.contactJid = jid;
 694    }
 695
 696    public Jid getNextCounterpart() {
 697        return this.nextCounterpart;
 698    }
 699
 700    public void setNextCounterpart(Jid jid) {
 701        this.nextCounterpart = jid;
 702    }
 703
 704    public int getNextEncryption() {
 705        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 706            return Message.ENCRYPTION_NONE;
 707        }
 708        if (OmemoSetting.isAlways()) {
 709            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 710        }
 711        final int defaultEncryption;
 712        if (suitableForOmemoByDefault(this)) {
 713            defaultEncryption = OmemoSetting.getEncryption();
 714        } else {
 715            defaultEncryption = Message.ENCRYPTION_NONE;
 716        }
 717        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 718        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 719            return defaultEncryption;
 720        } else {
 721            return encryption;
 722        }
 723    }
 724
 725    public boolean setNextEncryption(int encryption) {
 726        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 727    }
 728
 729    public String getNextMessage() {
 730        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 731        return nextMessage == null ? "" : nextMessage;
 732    }
 733
 734    public @Nullable
 735    Draft getDraft() {
 736        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 737        if (timestamp > getLatestMessage().getTimeSent()) {
 738            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 739            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 740                return new Draft(message, timestamp);
 741            }
 742        }
 743        return null;
 744    }
 745
 746    public boolean setNextMessage(final String input) {
 747        final String message = input == null || input.trim().isEmpty() ? null : input;
 748        boolean changed = !getNextMessage().equals(message);
 749        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 750        if (changed) {
 751            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 752        }
 753        return changed;
 754    }
 755
 756    public Bookmark getBookmark() {
 757        return this.account.getBookmark(this.contactJid);
 758    }
 759
 760    public Message findDuplicateMessage(Message message) {
 761        synchronized (this.messages) {
 762            for (int i = this.messages.size() - 1; i >= 0; --i) {
 763                if (this.messages.get(i).similar(message)) {
 764                    return this.messages.get(i);
 765                }
 766            }
 767        }
 768        return null;
 769    }
 770
 771    public boolean hasDuplicateMessage(Message message) {
 772        return findDuplicateMessage(message) != null;
 773    }
 774
 775    public Message findSentMessageWithBody(String body) {
 776        synchronized (this.messages) {
 777            for (int i = this.messages.size() - 1; i >= 0; --i) {
 778                Message message = this.messages.get(i);
 779                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 780                    String otherBody;
 781                    if (message.hasFileOnRemoteHost()) {
 782                        otherBody = message.getFileParams().url.toString();
 783                    } else {
 784                        otherBody = message.body;
 785                    }
 786                    if (otherBody != null && otherBody.equals(body)) {
 787                        return message;
 788                    }
 789                }
 790            }
 791            return null;
 792        }
 793    }
 794
 795    public Message findRtpSession(final String sessionId, final int s) {
 796        synchronized (this.messages) {
 797            for (int i = this.messages.size() - 1; i >= 0; --i) {
 798                final Message message = this.messages.get(i);
 799                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 800                    return message;
 801                }
 802            }
 803        }
 804        return null;
 805    }
 806
 807    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
 808        if (serverMsgId == null || remoteMsgId == null) {
 809            return false;
 810        }
 811        synchronized (this.messages) {
 812            for (Message message : this.messages) {
 813                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
 814                    return true;
 815                }
 816            }
 817        }
 818        return false;
 819    }
 820
 821    public MamReference getLastMessageTransmitted() {
 822        final MamReference lastClear = getLastClearHistory();
 823        MamReference lastReceived = new MamReference(0);
 824        synchronized (this.messages) {
 825            for (int i = this.messages.size() - 1; i >= 0; --i) {
 826                final Message message = this.messages.get(i);
 827                if (message.isPrivateMessage()) {
 828                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 829                }
 830                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 831                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
 832                    break;
 833                }
 834            }
 835        }
 836        return MamReference.max(lastClear, lastReceived);
 837    }
 838
 839    public void setMutedTill(long value) {
 840        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 841    }
 842
 843    public boolean isMuted() {
 844        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 845    }
 846
 847    public boolean alwaysNotify() {
 848        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
 849    }
 850
 851    public boolean setAttribute(String key, boolean value) {
 852        return setAttribute(key, String.valueOf(value));
 853    }
 854
 855    private boolean setAttribute(String key, long value) {
 856        return setAttribute(key, Long.toString(value));
 857    }
 858
 859    private boolean setAttribute(String key, int value) {
 860        return setAttribute(key, String.valueOf(value));
 861    }
 862
 863    public boolean setAttribute(String key, String value) {
 864        synchronized (this.attributes) {
 865            try {
 866                if (value == null) {
 867                    if (this.attributes.has(key)) {
 868                        this.attributes.remove(key);
 869                        return true;
 870                    } else {
 871                        return false;
 872                    }
 873                } else {
 874                    final String prev = this.attributes.optString(key, null);
 875                    this.attributes.put(key, value);
 876                    return !value.equals(prev);
 877                }
 878            } catch (JSONException e) {
 879                throw new AssertionError(e);
 880            }
 881        }
 882    }
 883
 884    public boolean setAttribute(String key, List<Jid> jids) {
 885        JSONArray array = new JSONArray();
 886        for (Jid jid : jids) {
 887            array.put(jid.asBareJid().toString());
 888        }
 889        synchronized (this.attributes) {
 890            try {
 891                this.attributes.put(key, array);
 892                return true;
 893            } catch (JSONException e) {
 894                return false;
 895            }
 896        }
 897    }
 898
 899    public String getAttribute(String key) {
 900        synchronized (this.attributes) {
 901            return this.attributes.optString(key, null);
 902        }
 903    }
 904
 905    private List<Jid> getJidListAttribute(String key) {
 906        ArrayList<Jid> list = new ArrayList<>();
 907        synchronized (this.attributes) {
 908            try {
 909                JSONArray array = this.attributes.getJSONArray(key);
 910                for (int i = 0; i < array.length(); ++i) {
 911                    try {
 912                        list.add(Jid.of(array.getString(i)));
 913                    } catch (IllegalArgumentException e) {
 914                        //ignored
 915                    }
 916                }
 917            } catch (JSONException e) {
 918                //ignored
 919            }
 920        }
 921        return list;
 922    }
 923
 924    private int getIntAttribute(String key, int defaultValue) {
 925        String value = this.getAttribute(key);
 926        if (value == null) {
 927            return defaultValue;
 928        } else {
 929            try {
 930                return Integer.parseInt(value);
 931            } catch (NumberFormatException e) {
 932                return defaultValue;
 933            }
 934        }
 935    }
 936
 937    public long getLongAttribute(String key, long defaultValue) {
 938        String value = this.getAttribute(key);
 939        if (value == null) {
 940            return defaultValue;
 941        } else {
 942            try {
 943                return Long.parseLong(value);
 944            } catch (NumberFormatException e) {
 945                return defaultValue;
 946            }
 947        }
 948    }
 949
 950    public boolean getBooleanAttribute(String key, boolean defaultValue) {
 951        String value = this.getAttribute(key);
 952        if (value == null) {
 953            return defaultValue;
 954        } else {
 955            return Boolean.parseBoolean(value);
 956        }
 957    }
 958
 959    public void add(Message message) {
 960        synchronized (this.messages) {
 961            this.messages.add(message);
 962        }
 963    }
 964
 965    public void prepend(int offset, Message message) {
 966        synchronized (this.messages) {
 967            this.messages.add(Math.min(offset, this.messages.size()), message);
 968        }
 969    }
 970
 971    public void addAll(int index, List<Message> messages) {
 972        synchronized (this.messages) {
 973            this.messages.addAll(index, messages);
 974        }
 975        account.getPgpDecryptionService().decrypt(messages);
 976    }
 977
 978    public void expireOldMessages(long timestamp) {
 979        synchronized (this.messages) {
 980            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
 981                if (iterator.next().getTimeSent() < timestamp) {
 982                    iterator.remove();
 983                }
 984            }
 985            untieMessages();
 986        }
 987    }
 988
 989    public void sort() {
 990        synchronized (this.messages) {
 991            Collections.sort(this.messages, (left, right) -> {
 992                if (left.getTimeSent() < right.getTimeSent()) {
 993                    return -1;
 994                } else if (left.getTimeSent() > right.getTimeSent()) {
 995                    return 1;
 996                } else {
 997                    return 0;
 998                }
 999            });
1000            untieMessages();
1001        }
1002    }
1003
1004    private void untieMessages() {
1005        for (Message message : this.messages) {
1006            message.untie();
1007        }
1008    }
1009
1010    public int unreadCount() {
1011        synchronized (this.messages) {
1012            int count = 0;
1013            for (int i = this.messages.size() - 1; i >= 0; --i) {
1014                if (this.messages.get(i).isRead()) {
1015                    return count;
1016                }
1017                ++count;
1018            }
1019            return count;
1020        }
1021    }
1022
1023    public int receivedMessagesCount() {
1024        int count = 0;
1025        synchronized (this.messages) {
1026            for (Message message : messages) {
1027                if (message.getStatus() == Message.STATUS_RECEIVED) {
1028                    ++count;
1029                }
1030            }
1031        }
1032        return count;
1033    }
1034
1035    public int sentMessagesCount() {
1036        int count = 0;
1037        synchronized (this.messages) {
1038            for (Message message : messages) {
1039                if (message.getStatus() != Message.STATUS_RECEIVED) {
1040                    ++count;
1041                }
1042            }
1043        }
1044        return count;
1045    }
1046
1047    public boolean isWithStranger() {
1048        final Contact contact = getContact();
1049        return mode == MODE_SINGLE
1050                && !contact.isOwnServer()
1051                && !contact.showInContactList()
1052                && !contact.isSelf()
1053                && !JidHelper.isQuicksyDomain(contact.getJid())
1054                && sentMessagesCount() == 0;
1055    }
1056
1057    public int getReceivedMessagesCountSinceUuid(String uuid) {
1058        if (uuid == null) {
1059            return 0;
1060        }
1061        int count = 0;
1062        synchronized (this.messages) {
1063            for (int i = messages.size() - 1; i >= 0; i--) {
1064                final Message message = messages.get(i);
1065                if (uuid.equals(message.getUuid())) {
1066                    return count;
1067                }
1068                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1069                    ++count;
1070                }
1071            }
1072        }
1073        return 0;
1074    }
1075
1076    @Override
1077    public int getAvatarBackgroundColor() {
1078        return UIHelper.getColorForName(getName().toString());
1079    }
1080
1081    @Override
1082    public String getAvatarName() {
1083        return getName().toString();
1084    }
1085
1086    public interface OnMessageFound {
1087        void onMessageFound(final Message message);
1088    }
1089
1090    public static class Draft {
1091        private final String message;
1092        private final long timestamp;
1093
1094        private Draft(String message, long timestamp) {
1095            this.message = message;
1096            this.timestamp = timestamp;
1097        }
1098
1099        public long getTimestamp() {
1100            return timestamp;
1101        }
1102
1103        public String getMessage() {
1104            return message;
1105        }
1106    }
1107}