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