Conversation.java

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