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    @NonNull
 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}