Conversation.java

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