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