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