Conversation.java

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