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        synchronized (this.messages) {
 554            for(final Message message : Lists.reverse(this.messages)) {
 555                if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
 556                    continue;
 557                }
 558                return message.isRead();
 559            }
 560            return true;
 561        }
 562    }
 563
 564    public List<Message> markRead(String upToUuid) {
 565        final List<Message> unread = new ArrayList<>();
 566        synchronized (this.messages) {
 567            for (Message message : this.messages) {
 568                if (!message.isRead()) {
 569                    message.markRead();
 570                    unread.add(message);
 571                }
 572                if (message.getUuid().equals(upToUuid)) {
 573                    return unread;
 574                }
 575            }
 576        }
 577        return unread;
 578    }
 579
 580    public Message getLatestMessage() {
 581        synchronized (this.messages) {
 582            if (this.messages.size() == 0) {
 583                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 584                message.setType(Message.TYPE_STATUS);
 585                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 586                return message;
 587            } else {
 588                return this.messages.get(this.messages.size() - 1);
 589            }
 590        }
 591    }
 592
 593    public @NonNull
 594    CharSequence getName() {
 595        if (getMode() == MODE_MULTI) {
 596            final String roomName = getMucOptions().getName();
 597            final String subject = getMucOptions().getSubject();
 598            final Bookmark bookmark = getBookmark();
 599            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 600            if (printableValue(roomName)) {
 601                return roomName;
 602            } else if (printableValue(subject)) {
 603                return subject;
 604            } else if (printableValue(bookmarkName, false)) {
 605                return bookmarkName;
 606            } else {
 607                final String generatedName = getMucOptions().createNameFromParticipants();
 608                if (printableValue(generatedName)) {
 609                    return generatedName;
 610                } else {
 611                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 612                }
 613            }
 614        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 615            return contactJid;
 616        } else {
 617            return this.getContact().getDisplayName();
 618        }
 619    }
 620
 621    public String getAccountUuid() {
 622        return this.accountUuid;
 623    }
 624
 625    public Account getAccount() {
 626        return this.account;
 627    }
 628
 629    public void setAccount(final Account account) {
 630        this.account = account;
 631    }
 632
 633    public Contact getContact() {
 634        return this.account.getRoster().getContact(this.contactJid);
 635    }
 636
 637    @Override
 638    public Jid getJid() {
 639        return this.contactJid;
 640    }
 641
 642    public int getStatus() {
 643        return this.status;
 644    }
 645
 646    public void setStatus(int status) {
 647        this.status = status;
 648    }
 649
 650    public long getCreated() {
 651        return this.created;
 652    }
 653
 654    public ContentValues getContentValues() {
 655        ContentValues values = new ContentValues();
 656        values.put(UUID, uuid);
 657        values.put(NAME, name);
 658        values.put(CONTACT, contactUuid);
 659        values.put(ACCOUNT, accountUuid);
 660        values.put(CONTACTJID, contactJid.toString());
 661        values.put(CREATED, created);
 662        values.put(STATUS, status);
 663        values.put(MODE, mode);
 664        synchronized (this.attributes) {
 665            values.put(ATTRIBUTES, attributes.toString());
 666        }
 667        return values;
 668    }
 669
 670    public int getMode() {
 671        return this.mode;
 672    }
 673
 674    public void setMode(int mode) {
 675        this.mode = mode;
 676    }
 677
 678    /**
 679     * short for is Private and Non-anonymous
 680     */
 681    public boolean isSingleOrPrivateAndNonAnonymous() {
 682        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 683    }
 684
 685    public boolean isPrivateAndNonAnonymous() {
 686        return getMucOptions().isPrivateAndNonAnonymous();
 687    }
 688
 689    public synchronized MucOptions getMucOptions() {
 690        if (this.mucOptions == null) {
 691            this.mucOptions = new MucOptions(this);
 692        }
 693        return this.mucOptions;
 694    }
 695
 696    public void resetMucOptions() {
 697        this.mucOptions = null;
 698    }
 699
 700    public void setContactJid(final Jid jid) {
 701        this.contactJid = jid;
 702    }
 703
 704    public Jid getNextCounterpart() {
 705        return this.nextCounterpart;
 706    }
 707
 708    public void setNextCounterpart(Jid jid) {
 709        this.nextCounterpart = jid;
 710    }
 711
 712    public int getNextEncryption() {
 713        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 714            return Message.ENCRYPTION_NONE;
 715        }
 716        if (OmemoSetting.isAlways()) {
 717            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 718        }
 719        final int defaultEncryption;
 720        if (suitableForOmemoByDefault(this)) {
 721            defaultEncryption = OmemoSetting.getEncryption();
 722        } else {
 723            defaultEncryption = Message.ENCRYPTION_NONE;
 724        }
 725        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 726        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 727            return defaultEncryption;
 728        } else {
 729            return encryption;
 730        }
 731    }
 732
 733    public boolean setNextEncryption(int encryption) {
 734        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 735    }
 736
 737    public String getNextMessage() {
 738        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 739        return nextMessage == null ? "" : nextMessage;
 740    }
 741
 742    public @Nullable
 743    Draft getDraft() {
 744        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 745        if (timestamp > getLatestMessage().getTimeSent()) {
 746            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 747            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 748                return new Draft(message, timestamp);
 749            }
 750        }
 751        return null;
 752    }
 753
 754    public boolean setNextMessage(final String input) {
 755        final String message = input == null || input.trim().isEmpty() ? null : input;
 756        boolean changed = !getNextMessage().equals(message);
 757        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 758        if (changed) {
 759            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 760        }
 761        return changed;
 762    }
 763
 764    public Bookmark getBookmark() {
 765        return this.account.getBookmark(this.contactJid);
 766    }
 767
 768    public Message findDuplicateMessage(Message message) {
 769        synchronized (this.messages) {
 770            for (int i = this.messages.size() - 1; i >= 0; --i) {
 771                if (this.messages.get(i).similar(message)) {
 772                    return this.messages.get(i);
 773                }
 774            }
 775        }
 776        return null;
 777    }
 778
 779    public boolean hasDuplicateMessage(Message message) {
 780        return findDuplicateMessage(message) != null;
 781    }
 782
 783    public Message findSentMessageWithBody(String body) {
 784        synchronized (this.messages) {
 785            for (int i = this.messages.size() - 1; i >= 0; --i) {
 786                Message message = this.messages.get(i);
 787                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 788                    String otherBody;
 789                    if (message.hasFileOnRemoteHost()) {
 790                        otherBody = message.getFileParams().url.toString();
 791                    } else {
 792                        otherBody = message.body;
 793                    }
 794                    if (otherBody != null && otherBody.equals(body)) {
 795                        return message;
 796                    }
 797                }
 798            }
 799            return null;
 800        }
 801    }
 802
 803    public Message findRtpSession(final String sessionId, final int s) {
 804        synchronized (this.messages) {
 805            for (int i = this.messages.size() - 1; i >= 0; --i) {
 806                final Message message = this.messages.get(i);
 807                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 808                    return message;
 809                }
 810            }
 811        }
 812        return null;
 813    }
 814
 815    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
 816        if (serverMsgId == null || remoteMsgId == null) {
 817            return false;
 818        }
 819        synchronized (this.messages) {
 820            for (Message message : this.messages) {
 821                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
 822                    return true;
 823                }
 824            }
 825        }
 826        return false;
 827    }
 828
 829    public MamReference getLastMessageTransmitted() {
 830        final MamReference lastClear = getLastClearHistory();
 831        MamReference lastReceived = new MamReference(0);
 832        synchronized (this.messages) {
 833            for (int i = this.messages.size() - 1; i >= 0; --i) {
 834                final Message message = this.messages.get(i);
 835                if (message.isPrivateMessage()) {
 836                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 837                }
 838                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 839                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
 840                    break;
 841                }
 842            }
 843        }
 844        return MamReference.max(lastClear, lastReceived);
 845    }
 846
 847    public void setMutedTill(long value) {
 848        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 849    }
 850
 851    public boolean isMuted() {
 852        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 853    }
 854
 855    public boolean alwaysNotify() {
 856        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
 857    }
 858
 859    public boolean setAttribute(String key, boolean value) {
 860        return setAttribute(key, String.valueOf(value));
 861    }
 862
 863    private boolean setAttribute(String key, long value) {
 864        return setAttribute(key, Long.toString(value));
 865    }
 866
 867    private boolean setAttribute(String key, int value) {
 868        return setAttribute(key, String.valueOf(value));
 869    }
 870
 871    public boolean setAttribute(String key, String value) {
 872        synchronized (this.attributes) {
 873            try {
 874                if (value == null) {
 875                    if (this.attributes.has(key)) {
 876                        this.attributes.remove(key);
 877                        return true;
 878                    } else {
 879                        return false;
 880                    }
 881                } else {
 882                    final String prev = this.attributes.optString(key, null);
 883                    this.attributes.put(key, value);
 884                    return !value.equals(prev);
 885                }
 886            } catch (JSONException e) {
 887                throw new AssertionError(e);
 888            }
 889        }
 890    }
 891
 892    public boolean setAttribute(String key, List<Jid> jids) {
 893        JSONArray array = new JSONArray();
 894        for (Jid jid : jids) {
 895            array.put(jid.asBareJid().toString());
 896        }
 897        synchronized (this.attributes) {
 898            try {
 899                this.attributes.put(key, array);
 900                return true;
 901            } catch (JSONException e) {
 902                return false;
 903            }
 904        }
 905    }
 906
 907    public String getAttribute(String key) {
 908        synchronized (this.attributes) {
 909            return this.attributes.optString(key, null);
 910        }
 911    }
 912
 913    private List<Jid> getJidListAttribute(String key) {
 914        ArrayList<Jid> list = new ArrayList<>();
 915        synchronized (this.attributes) {
 916            try {
 917                JSONArray array = this.attributes.getJSONArray(key);
 918                for (int i = 0; i < array.length(); ++i) {
 919                    try {
 920                        list.add(Jid.of(array.getString(i)));
 921                    } catch (IllegalArgumentException e) {
 922                        //ignored
 923                    }
 924                }
 925            } catch (JSONException e) {
 926                //ignored
 927            }
 928        }
 929        return list;
 930    }
 931
 932    private int getIntAttribute(String key, int defaultValue) {
 933        String value = this.getAttribute(key);
 934        if (value == null) {
 935            return defaultValue;
 936        } else {
 937            try {
 938                return Integer.parseInt(value);
 939            } catch (NumberFormatException e) {
 940                return defaultValue;
 941            }
 942        }
 943    }
 944
 945    public long getLongAttribute(String key, long defaultValue) {
 946        String value = this.getAttribute(key);
 947        if (value == null) {
 948            return defaultValue;
 949        } else {
 950            try {
 951                return Long.parseLong(value);
 952            } catch (NumberFormatException e) {
 953                return defaultValue;
 954            }
 955        }
 956    }
 957
 958    public boolean getBooleanAttribute(String key, boolean defaultValue) {
 959        String value = this.getAttribute(key);
 960        if (value == null) {
 961            return defaultValue;
 962        } else {
 963            return Boolean.parseBoolean(value);
 964        }
 965    }
 966
 967    public void add(Message message) {
 968        synchronized (this.messages) {
 969            this.messages.add(message);
 970        }
 971    }
 972
 973    public void prepend(int offset, Message message) {
 974        synchronized (this.messages) {
 975            this.messages.add(Math.min(offset, this.messages.size()), message);
 976        }
 977    }
 978
 979    public void addAll(int index, List<Message> messages) {
 980        synchronized (this.messages) {
 981            this.messages.addAll(index, messages);
 982        }
 983        account.getPgpDecryptionService().decrypt(messages);
 984    }
 985
 986    public void expireOldMessages(long timestamp) {
 987        synchronized (this.messages) {
 988            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
 989                if (iterator.next().getTimeSent() < timestamp) {
 990                    iterator.remove();
 991                }
 992            }
 993            untieMessages();
 994        }
 995    }
 996
 997    public void sort() {
 998        synchronized (this.messages) {
 999            Collections.sort(this.messages, (left, right) -> {
1000                if (left.getTimeSent() < right.getTimeSent()) {
1001                    return -1;
1002                } else if (left.getTimeSent() > right.getTimeSent()) {
1003                    return 1;
1004                } else {
1005                    return 0;
1006                }
1007            });
1008            untieMessages();
1009        }
1010    }
1011
1012    private void untieMessages() {
1013        for (Message message : this.messages) {
1014            message.untie();
1015        }
1016    }
1017
1018    public int unreadCount() {
1019        synchronized (this.messages) {
1020            int count = 0;
1021            for(final Message message : Lists.reverse(this.messages)) {
1022                if (message.isRead()) {
1023                    if (message.getType() == Message.TYPE_RTP_SESSION) {
1024                        continue;
1025                    }
1026                    return count;
1027                }
1028                ++count;
1029            }
1030            return count;
1031        }
1032    }
1033
1034    public int receivedMessagesCount() {
1035        int count = 0;
1036        synchronized (this.messages) {
1037            for (Message message : messages) {
1038                if (message.getStatus() == Message.STATUS_RECEIVED) {
1039                    ++count;
1040                }
1041            }
1042        }
1043        return count;
1044    }
1045
1046    public int sentMessagesCount() {
1047        int count = 0;
1048        synchronized (this.messages) {
1049            for (Message message : messages) {
1050                if (message.getStatus() != Message.STATUS_RECEIVED) {
1051                    ++count;
1052                }
1053            }
1054        }
1055        return count;
1056    }
1057
1058    public boolean isWithStranger() {
1059        final Contact contact = getContact();
1060        return mode == MODE_SINGLE
1061                && !contact.isOwnServer()
1062                && !contact.showInContactList()
1063                && !contact.isSelf()
1064                && !JidHelper.isQuicksyDomain(contact.getJid())
1065                && sentMessagesCount() == 0;
1066    }
1067
1068    public int getReceivedMessagesCountSinceUuid(String uuid) {
1069        if (uuid == null) {
1070            return 0;
1071        }
1072        int count = 0;
1073        synchronized (this.messages) {
1074            for (int i = messages.size() - 1; i >= 0; i--) {
1075                final Message message = messages.get(i);
1076                if (uuid.equals(message.getUuid())) {
1077                    return count;
1078                }
1079                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1080                    ++count;
1081                }
1082            }
1083        }
1084        return 0;
1085    }
1086
1087    @Override
1088    public int getAvatarBackgroundColor() {
1089        return UIHelper.getColorForName(getName().toString());
1090    }
1091
1092    @Override
1093    public String getAvatarName() {
1094        return getName().toString();
1095    }
1096
1097    public interface OnMessageFound {
1098        void onMessageFound(final Message message);
1099    }
1100
1101    public static class Draft {
1102        private final String message;
1103        private final long timestamp;
1104
1105        private Draft(String message, long timestamp) {
1106            this.message = message;
1107            this.timestamp = timestamp;
1108        }
1109
1110        public long getTimestamp() {
1111            return timestamp;
1112        }
1113
1114        public String getMessage() {
1115            return message;
1116        }
1117    }
1118}