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