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 (Message message : this.messages) {
 200                if (!message.isRead()) {
 201                    results.add(message);
 202                }
 203            }
 204        }
 205        for (Message result : results) {
 206            onMessageFound.onMessageFound(result);
 207        }
 208    }
 209
 210    public Message findMessageWithFileAndUuid(final String uuid) {
 211        synchronized (this.messages) {
 212            for (final Message message : this.messages) {
 213                if (message.getUuid().equals(uuid)
 214                        && message.getEncryption() != Message.ENCRYPTION_PGP
 215                        && (message.isFileOrImage() || message.treatAsDownloadable())) {
 216                    return message;
 217                }
 218            }
 219        }
 220        return null;
 221    }
 222
 223    public boolean markAsDeleted(final List<String> uuids) {
 224        boolean deleted = false;
 225        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 226        synchronized (this.messages) {
 227            for (Message message : this.messages) {
 228                if (uuids.contains(message.getUuid())) {
 229                    message.setDeleted(true);
 230                    deleted = true;
 231                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 232                        pgpDecryptionService.discard(message);
 233                    }
 234                }
 235            }
 236        }
 237        return deleted;
 238    }
 239
 240    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
 241        boolean changed = false;
 242        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 243        synchronized (this.messages) {
 244            for (Message message : this.messages) {
 245                for (final DatabaseBackend.FilePathInfo file : files)
 246                    if (file.uuid.toString().equals(message.getUuid())) {
 247                        message.setDeleted(file.deleted);
 248                        changed = true;
 249                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 250                            pgpDecryptionService.discard(message);
 251                        }
 252                    }
 253            }
 254        }
 255        return changed;
 256    }
 257
 258    public void clearMessages() {
 259        synchronized (this.messages) {
 260            this.messages.clear();
 261        }
 262    }
 263
 264    public boolean setIncomingChatState(ChatState state) {
 265        if (this.mIncomingChatState == state) {
 266            return false;
 267        }
 268        this.mIncomingChatState = state;
 269        return true;
 270    }
 271
 272    public ChatState getIncomingChatState() {
 273        return this.mIncomingChatState;
 274    }
 275
 276    public boolean setOutgoingChatState(ChatState state) {
 277        if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
 278            if (this.mOutgoingChatState != state) {
 279                this.mOutgoingChatState = state;
 280                return true;
 281            }
 282        }
 283        return false;
 284    }
 285
 286    public ChatState getOutgoingChatState() {
 287        return this.mOutgoingChatState;
 288    }
 289
 290    public void trim() {
 291        synchronized (this.messages) {
 292            final int size = messages.size();
 293            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 294            if (size > maxsize) {
 295                List<Message> discards = this.messages.subList(0, size - maxsize);
 296                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 297                if (pgpDecryptionService != null) {
 298                    pgpDecryptionService.discard(discards);
 299                }
 300                discards.clear();
 301                untieMessages();
 302            }
 303        }
 304    }
 305
 306    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 307        final ArrayList<Message> results = new ArrayList<>();
 308        synchronized (this.messages) {
 309            for (Message message : this.messages) {
 310                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
 311                    results.add(message);
 312                }
 313            }
 314        }
 315        for (Message result : results) {
 316            onMessageFound.onMessageFound(result);
 317        }
 318    }
 319
 320    public Message findSentMessageWithUuidOrRemoteId(String id) {
 321        synchronized (this.messages) {
 322            for (Message message : this.messages) {
 323                if (id.equals(message.getUuid())
 324                        || (message.getStatus() >= Message.STATUS_SEND
 325                        && id.equals(message.getRemoteMsgId()))) {
 326                    return message;
 327                }
 328            }
 329        }
 330        return null;
 331    }
 332
 333    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
 334        synchronized (this.messages) {
 335            for (int i = this.messages.size() - 1; i >= 0; --i) {
 336                final Message message = messages.get(i);
 337                final Jid mcp = message.getCounterpart();
 338                if (mcp == null) {
 339                    continue;
 340                }
 341                final boolean counterpartMatch = mode == MODE_SINGLE ?
 342                        counterpart.asBareJid().equals(mcp.asBareJid()) :
 343                        counterpart.equals(mcp);
 344                if (counterpartMatch && ((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 Long.compare(another.getSortableTime(), getSortableTime());
 485    }
 486
 487    private long getSortableTime() {
 488        Draft draft = getDraft();
 489        long messageTime = getLatestMessage().getTimeSent();
 490        if (draft == null) {
 491            return messageTime;
 492        } else {
 493            return Math.max(messageTime, draft.getTimestamp());
 494        }
 495    }
 496
 497    public String getDraftMessage() {
 498        return draftMessage;
 499    }
 500
 501    public void setDraftMessage(String draftMessage) {
 502        this.draftMessage = draftMessage;
 503    }
 504
 505    public boolean isRead() {
 506        return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
 507    }
 508
 509    public List<Message> markRead(String upToUuid) {
 510        final List<Message> unread = new ArrayList<>();
 511        synchronized (this.messages) {
 512            for (Message message : this.messages) {
 513                if (!message.isRead()) {
 514                    message.markRead();
 515                    unread.add(message);
 516                }
 517                if (message.getUuid().equals(upToUuid)) {
 518                    return unread;
 519                }
 520            }
 521        }
 522        return unread;
 523    }
 524
 525    public Message getLatestMessage() {
 526        synchronized (this.messages) {
 527            if (this.messages.size() == 0) {
 528                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 529                message.setType(Message.TYPE_STATUS);
 530                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 531                return message;
 532            } else {
 533                return this.messages.get(this.messages.size() - 1);
 534            }
 535        }
 536    }
 537
 538    public @NonNull
 539    CharSequence getName() {
 540        if (getMode() == MODE_MULTI) {
 541            final String roomName = getMucOptions().getName();
 542            final String subject = getMucOptions().getSubject();
 543            final Bookmark bookmark = getBookmark();
 544            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 545            if (printableValue(roomName)) {
 546                return roomName;
 547            } else if (printableValue(subject)) {
 548                return subject;
 549            } else if (printableValue(bookmarkName, false)) {
 550                return bookmarkName;
 551            } else {
 552                final String generatedName = getMucOptions().createNameFromParticipants();
 553                if (printableValue(generatedName)) {
 554                    return generatedName;
 555                } else {
 556                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 557                }
 558            }
 559        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 560            return contactJid;
 561        } else {
 562            return this.getContact().getDisplayName();
 563        }
 564    }
 565
 566    public String getAccountUuid() {
 567        return this.accountUuid;
 568    }
 569
 570    public Account getAccount() {
 571        return this.account;
 572    }
 573
 574    public void setAccount(final Account account) {
 575        this.account = account;
 576    }
 577
 578    public Contact getContact() {
 579        return this.account.getRoster().getContact(this.contactJid);
 580    }
 581
 582    @Override
 583    public Jid getJid() {
 584        return this.contactJid;
 585    }
 586
 587    public int getStatus() {
 588        return this.status;
 589    }
 590
 591    public void setStatus(int status) {
 592        this.status = status;
 593    }
 594
 595    public long getCreated() {
 596        return this.created;
 597    }
 598
 599    public ContentValues getContentValues() {
 600        ContentValues values = new ContentValues();
 601        values.put(UUID, uuid);
 602        values.put(NAME, name);
 603        values.put(CONTACT, contactUuid);
 604        values.put(ACCOUNT, accountUuid);
 605        values.put(CONTACTJID, contactJid.toString());
 606        values.put(CREATED, created);
 607        values.put(STATUS, status);
 608        values.put(MODE, mode);
 609        synchronized (this.attributes) {
 610            values.put(ATTRIBUTES, attributes.toString());
 611        }
 612        return values;
 613    }
 614
 615    public int getMode() {
 616        return this.mode;
 617    }
 618
 619    public void setMode(int mode) {
 620        this.mode = mode;
 621    }
 622
 623    /**
 624     * short for is Private and Non-anonymous
 625     */
 626    public boolean isSingleOrPrivateAndNonAnonymous() {
 627        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 628    }
 629
 630    public boolean isPrivateAndNonAnonymous() {
 631        return getMucOptions().isPrivateAndNonAnonymous();
 632    }
 633
 634    public synchronized MucOptions getMucOptions() {
 635        if (this.mucOptions == null) {
 636            this.mucOptions = new MucOptions(this);
 637        }
 638        return this.mucOptions;
 639    }
 640
 641    public void resetMucOptions() {
 642        this.mucOptions = null;
 643    }
 644
 645    public void setContactJid(final Jid jid) {
 646        this.contactJid = jid;
 647    }
 648
 649    public Jid getNextCounterpart() {
 650        return this.nextCounterpart;
 651    }
 652
 653    public void setNextCounterpart(Jid jid) {
 654        this.nextCounterpart = jid;
 655    }
 656
 657    public int getNextEncryption() {
 658        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 659            return Message.ENCRYPTION_NONE;
 660        }
 661        if (OmemoSetting.isAlways()) {
 662            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 663        }
 664        final int defaultEncryption;
 665        if (suitableForOmemoByDefault(this)) {
 666            defaultEncryption = OmemoSetting.getEncryption();
 667        } else {
 668            defaultEncryption = Message.ENCRYPTION_NONE;
 669        }
 670        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 671        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 672            return defaultEncryption;
 673        } else {
 674            return encryption;
 675        }
 676    }
 677
 678    public boolean setNextEncryption(int encryption) {
 679        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 680    }
 681
 682    public String getNextMessage() {
 683        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 684        return nextMessage == null ? "" : nextMessage;
 685    }
 686
 687    public @Nullable
 688    Draft getDraft() {
 689        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 690        if (timestamp > getLatestMessage().getTimeSent()) {
 691            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 692            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 693                return new Draft(message, timestamp);
 694            }
 695        }
 696        return null;
 697    }
 698
 699    public boolean setNextMessage(final String input) {
 700        final String message = input == null || input.trim().isEmpty() ? null : input;
 701        boolean changed = !getNextMessage().equals(message);
 702        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 703        if (changed) {
 704            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 705        }
 706        return changed;
 707    }
 708
 709    public Bookmark getBookmark() {
 710        return this.account.getBookmark(this.contactJid);
 711    }
 712
 713    public Message findDuplicateMessage(Message message) {
 714        synchronized (this.messages) {
 715            for (int i = this.messages.size() - 1; i >= 0; --i) {
 716                if (this.messages.get(i).similar(message)) {
 717                    return this.messages.get(i);
 718                }
 719            }
 720        }
 721        return null;
 722    }
 723
 724    public boolean hasDuplicateMessage(Message message) {
 725        return findDuplicateMessage(message) != null;
 726    }
 727
 728    public Message findSentMessageWithBody(String body) {
 729        synchronized (this.messages) {
 730            for (int i = this.messages.size() - 1; i >= 0; --i) {
 731                Message message = this.messages.get(i);
 732                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 733                    String otherBody;
 734                    if (message.hasFileOnRemoteHost()) {
 735                        otherBody = message.getFileParams().url.toString();
 736                    } else {
 737                        otherBody = message.body;
 738                    }
 739                    if (otherBody != null && otherBody.equals(body)) {
 740                        return message;
 741                    }
 742                }
 743            }
 744            return null;
 745        }
 746    }
 747
 748    public Message findRtpSession(final String sessionId, final int s) {
 749        synchronized (this.messages) {
 750            for (int i = this.messages.size() - 1; i >= 0; --i) {
 751                final Message message = this.messages.get(i);
 752                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 753                    return message;
 754                }
 755            }
 756        }
 757        return null;
 758    }
 759
 760    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
 761        if (serverMsgId == null || remoteMsgId == null) {
 762            return false;
 763        }
 764        synchronized (this.messages) {
 765            for (Message message : this.messages) {
 766                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
 767                    return true;
 768                }
 769            }
 770        }
 771        return false;
 772    }
 773
 774    public MamReference getLastMessageTransmitted() {
 775        final MamReference lastClear = getLastClearHistory();
 776        MamReference lastReceived = new MamReference(0);
 777        synchronized (this.messages) {
 778            for (int i = this.messages.size() - 1; i >= 0; --i) {
 779                final Message message = this.messages.get(i);
 780                if (message.isPrivateMessage()) {
 781                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 782                }
 783                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 784                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
 785                    break;
 786                }
 787            }
 788        }
 789        return MamReference.max(lastClear, lastReceived);
 790    }
 791
 792    public void setMutedTill(long value) {
 793        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 794    }
 795
 796    public boolean isMuted() {
 797        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 798    }
 799
 800    public boolean alwaysNotify() {
 801        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
 802    }
 803
 804    public boolean setAttribute(String key, boolean value) {
 805        return setAttribute(key, String.valueOf(value));
 806    }
 807
 808    private boolean setAttribute(String key, long value) {
 809        return setAttribute(key, Long.toString(value));
 810    }
 811
 812    private boolean setAttribute(String key, int value) {
 813        return setAttribute(key, String.valueOf(value));
 814    }
 815
 816    public boolean setAttribute(String key, String value) {
 817        synchronized (this.attributes) {
 818            try {
 819                if (value == null) {
 820                    if (this.attributes.has(key)) {
 821                        this.attributes.remove(key);
 822                        return true;
 823                    } else {
 824                        return false;
 825                    }
 826                } else {
 827                    final String prev = this.attributes.optString(key, null);
 828                    this.attributes.put(key, value);
 829                    return !value.equals(prev);
 830                }
 831            } catch (JSONException e) {
 832                throw new AssertionError(e);
 833            }
 834        }
 835    }
 836
 837    public boolean setAttribute(String key, List<Jid> jids) {
 838        JSONArray array = new JSONArray();
 839        for (Jid jid : jids) {
 840            array.put(jid.asBareJid().toString());
 841        }
 842        synchronized (this.attributes) {
 843            try {
 844                this.attributes.put(key, array);
 845                return true;
 846            } catch (JSONException e) {
 847                return false;
 848            }
 849        }
 850    }
 851
 852    public String getAttribute(String key) {
 853        synchronized (this.attributes) {
 854            return this.attributes.optString(key, null);
 855        }
 856    }
 857
 858    private List<Jid> getJidListAttribute(String key) {
 859        ArrayList<Jid> list = new ArrayList<>();
 860        synchronized (this.attributes) {
 861            try {
 862                JSONArray array = this.attributes.getJSONArray(key);
 863                for (int i = 0; i < array.length(); ++i) {
 864                    try {
 865                        list.add(Jid.of(array.getString(i)));
 866                    } catch (IllegalArgumentException e) {
 867                        //ignored
 868                    }
 869                }
 870            } catch (JSONException e) {
 871                //ignored
 872            }
 873        }
 874        return list;
 875    }
 876
 877    private int getIntAttribute(String key, int defaultValue) {
 878        String value = this.getAttribute(key);
 879        if (value == null) {
 880            return defaultValue;
 881        } else {
 882            try {
 883                return Integer.parseInt(value);
 884            } catch (NumberFormatException e) {
 885                return defaultValue;
 886            }
 887        }
 888    }
 889
 890    public long getLongAttribute(String key, long defaultValue) {
 891        String value = this.getAttribute(key);
 892        if (value == null) {
 893            return defaultValue;
 894        } else {
 895            try {
 896                return Long.parseLong(value);
 897            } catch (NumberFormatException e) {
 898                return defaultValue;
 899            }
 900        }
 901    }
 902
 903    public boolean getBooleanAttribute(String key, boolean defaultValue) {
 904        String value = this.getAttribute(key);
 905        if (value == null) {
 906            return defaultValue;
 907        } else {
 908            return Boolean.parseBoolean(value);
 909        }
 910    }
 911
 912    public void add(Message message) {
 913        synchronized (this.messages) {
 914            this.messages.add(message);
 915        }
 916    }
 917
 918    public void prepend(int offset, Message message) {
 919        synchronized (this.messages) {
 920            this.messages.add(Math.min(offset, this.messages.size()), message);
 921        }
 922    }
 923
 924    public void addAll(int index, List<Message> messages) {
 925        synchronized (this.messages) {
 926            this.messages.addAll(index, messages);
 927        }
 928        account.getPgpDecryptionService().decrypt(messages);
 929    }
 930
 931    public void expireOldMessages(long timestamp) {
 932        synchronized (this.messages) {
 933            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
 934                if (iterator.next().getTimeSent() < timestamp) {
 935                    iterator.remove();
 936                }
 937            }
 938            untieMessages();
 939        }
 940    }
 941
 942    public void sort() {
 943        synchronized (this.messages) {
 944            Collections.sort(this.messages, (left, right) -> {
 945                if (left.getTimeSent() < right.getTimeSent()) {
 946                    return -1;
 947                } else if (left.getTimeSent() > right.getTimeSent()) {
 948                    return 1;
 949                } else {
 950                    return 0;
 951                }
 952            });
 953            untieMessages();
 954        }
 955    }
 956
 957    private void untieMessages() {
 958        for (Message message : this.messages) {
 959            message.untie();
 960        }
 961    }
 962
 963    public int unreadCount() {
 964        synchronized (this.messages) {
 965            int count = 0;
 966            for (int i = this.messages.size() - 1; i >= 0; --i) {
 967                if (this.messages.get(i).isRead()) {
 968                    return count;
 969                }
 970                ++count;
 971            }
 972            return count;
 973        }
 974    }
 975
 976    public int receivedMessagesCount() {
 977        int count = 0;
 978        synchronized (this.messages) {
 979            for (Message message : messages) {
 980                if (message.getStatus() == Message.STATUS_RECEIVED) {
 981                    ++count;
 982                }
 983            }
 984        }
 985        return count;
 986    }
 987
 988    public int sentMessagesCount() {
 989        int count = 0;
 990        synchronized (this.messages) {
 991            for (Message message : messages) {
 992                if (message.getStatus() != Message.STATUS_RECEIVED) {
 993                    ++count;
 994                }
 995            }
 996        }
 997        return count;
 998    }
 999
1000    public boolean isWithStranger() {
1001        final Contact contact = getContact();
1002        return mode == MODE_SINGLE
1003                && !contact.isOwnServer()
1004                && !contact.showInContactList()
1005                && !contact.isSelf()
1006                && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString())
1007                && sentMessagesCount() == 0;
1008    }
1009
1010    public int getReceivedMessagesCountSinceUuid(String uuid) {
1011        if (uuid == null) {
1012            return 0;
1013        }
1014        int count = 0;
1015        synchronized (this.messages) {
1016            for (int i = messages.size() - 1; i >= 0; i--) {
1017                final Message message = messages.get(i);
1018                if (uuid.equals(message.getUuid())) {
1019                    return count;
1020                }
1021                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1022                    ++count;
1023                }
1024            }
1025        }
1026        return 0;
1027    }
1028
1029    @Override
1030    public int getAvatarBackgroundColor() {
1031        return UIHelper.getColorForName(getName().toString());
1032    }
1033
1034    public interface OnMessageFound {
1035        void onMessageFound(final Message message);
1036    }
1037
1038    public static class Draft {
1039        private final String message;
1040        private final long timestamp;
1041
1042        private Draft(String message, long timestamp) {
1043            this.message = message;
1044            this.timestamp = timestamp;
1045        }
1046
1047        public long getTimestamp() {
1048            return timestamp;
1049        }
1050
1051        public String getMessage() {
1052            return message;
1053        }
1054    }
1055}