Conversation.java

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