Conversation.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.net.Uri;
   6import android.text.Editable;
   7import android.text.TextUtils;
   8import android.text.TextWatcher;
   9import android.view.LayoutInflater;
  10import android.view.View;
  11import android.view.ViewGroup;
  12import android.widget.ArrayAdapter;
  13import android.webkit.WebView;
  14import android.webkit.WebViewClient;
  15
  16import androidx.annotation.NonNull;
  17import androidx.annotation.Nullable;
  18import androidx.databinding.DataBindingUtil;
  19import androidx.databinding.ViewDataBinding;
  20import androidx.viewpager.widget.PagerAdapter;
  21import androidx.recyclerview.widget.RecyclerView;
  22import androidx.recyclerview.widget.LinearLayoutManager;
  23import androidx.viewpager.widget.ViewPager;
  24
  25import com.google.android.material.tabs.TabLayout;
  26import com.google.common.collect.ComparisonChain;
  27import com.google.common.collect.Lists;
  28
  29import org.json.JSONArray;
  30import org.json.JSONException;
  31import org.json.JSONObject;
  32
  33import java.util.ArrayList;
  34import java.util.Collections;
  35import java.util.Iterator;
  36import java.util.List;
  37import java.util.ListIterator;
  38import java.util.concurrent.atomic.AtomicBoolean;
  39
  40import eu.siacs.conversations.Config;
  41import eu.siacs.conversations.R;
  42import eu.siacs.conversations.crypto.OmemoSetting;
  43import eu.siacs.conversations.crypto.PgpDecryptionService;
  44import eu.siacs.conversations.databinding.CommandPageBinding;
  45import eu.siacs.conversations.databinding.CommandNoteBinding;
  46import eu.siacs.conversations.databinding.CommandResultFieldBinding;
  47import eu.siacs.conversations.databinding.CommandTextFieldBinding;
  48import eu.siacs.conversations.databinding.CommandWebviewBinding;
  49import eu.siacs.conversations.persistance.DatabaseBackend;
  50import eu.siacs.conversations.services.AvatarService;
  51import eu.siacs.conversations.services.QuickConversationsService;
  52import eu.siacs.conversations.services.XmppConnectionService;
  53import eu.siacs.conversations.utils.JidHelper;
  54import eu.siacs.conversations.utils.MessageUtils;
  55import eu.siacs.conversations.utils.UIHelper;
  56import eu.siacs.conversations.xml.Element;
  57import eu.siacs.conversations.xml.Namespace;
  58import eu.siacs.conversations.xmpp.Jid;
  59import eu.siacs.conversations.xmpp.chatstate.ChatState;
  60import eu.siacs.conversations.xmpp.mam.MamReference;
  61import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  62
  63import static eu.siacs.conversations.entities.Bookmark.printableValue;
  64
  65
  66public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
  67    public static final String TABLENAME = "conversations";
  68
  69    public static final int STATUS_AVAILABLE = 0;
  70    public static final int STATUS_ARCHIVED = 1;
  71
  72    public static final String NAME = "name";
  73    public static final String ACCOUNT = "accountUuid";
  74    public static final String CONTACT = "contactUuid";
  75    public static final String CONTACTJID = "contactJid";
  76    public static final String STATUS = "status";
  77    public static final String CREATED = "created";
  78    public static final String MODE = "mode";
  79    public static final String ATTRIBUTES = "attributes";
  80
  81    public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
  82    public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
  83    public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
  84    public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
  85    public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
  86    static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
  87    static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
  88    static final String ATTRIBUTE_MODERATED = "moderated";
  89    static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
  90    private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
  91    private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
  92    private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
  93    private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
  94    private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
  95    protected final ArrayList<Message> messages = new ArrayList<>();
  96    public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
  97    protected Account account = null;
  98    private String draftMessage;
  99    private final String name;
 100    private final String contactUuid;
 101    private final String accountUuid;
 102    private Jid contactJid;
 103    private int status;
 104    private final long created;
 105    private int mode;
 106    private JSONObject attributes;
 107    private Jid nextCounterpart;
 108    private transient MucOptions mucOptions = null;
 109    private boolean messagesLeftOnServer = true;
 110    private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
 111    private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
 112    private String mFirstMamReference = null;
 113    protected int mCurrentTab = -1;
 114    protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
 115
 116    public Conversation(final String name, final Account account, final Jid contactJid,
 117                        final int mode) {
 118        this(java.util.UUID.randomUUID().toString(), name, null, account
 119                        .getUuid(), contactJid, System.currentTimeMillis(),
 120                STATUS_AVAILABLE, mode, "");
 121        this.account = account;
 122    }
 123
 124    public Conversation(final String uuid, final String name, final String contactUuid,
 125                        final String accountUuid, final Jid contactJid, final long created, final int status,
 126                        final int mode, final String attributes) {
 127        this.uuid = uuid;
 128        this.name = name;
 129        this.contactUuid = contactUuid;
 130        this.accountUuid = accountUuid;
 131        this.contactJid = contactJid;
 132        this.created = created;
 133        this.status = status;
 134        this.mode = mode;
 135        try {
 136            this.attributes = new JSONObject(attributes == null ? "" : attributes);
 137        } catch (JSONException e) {
 138            this.attributes = new JSONObject();
 139        }
 140    }
 141
 142    public static Conversation fromCursor(Cursor cursor) {
 143        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 144                cursor.getString(cursor.getColumnIndex(NAME)),
 145                cursor.getString(cursor.getColumnIndex(CONTACT)),
 146                cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 147                JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
 148                cursor.getLong(cursor.getColumnIndex(CREATED)),
 149                cursor.getInt(cursor.getColumnIndex(STATUS)),
 150                cursor.getInt(cursor.getColumnIndex(MODE)),
 151                cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 152    }
 153
 154    public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
 155        for (int i = messages.size() - 1; i >= 0; --i) {
 156            final Message message = messages.get(i);
 157            if (message.getStatus() <= Message.STATUS_RECEIVED
 158                    && (message.markable || isPrivateAndNonAnonymousMuc)
 159                    && !message.isPrivateMessage()) {
 160                return message;
 161            }
 162        }
 163        return null;
 164    }
 165
 166    private static boolean suitableForOmemoByDefault(final Conversation conversation) {
 167        if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
 168            return false;
 169        }
 170        if (conversation.getContact().isOwnServer()) {
 171            return false;
 172        }
 173        final String contact = conversation.getJid().getDomain().toEscapedString();
 174        final String account = conversation.getAccount().getServer();
 175        if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
 176            return false;
 177        }
 178        return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
 179    }
 180
 181    public boolean hasMessagesLeftOnServer() {
 182        return messagesLeftOnServer;
 183    }
 184
 185    public void setHasMessagesLeftOnServer(boolean value) {
 186        this.messagesLeftOnServer = value;
 187    }
 188
 189    public Message getFirstUnreadMessage() {
 190        Message first = null;
 191        synchronized (this.messages) {
 192            for (int i = messages.size() - 1; i >= 0; --i) {
 193                if (messages.get(i).isRead()) {
 194                    return first;
 195                } else {
 196                    first = messages.get(i);
 197                }
 198            }
 199        }
 200        return first;
 201    }
 202
 203    public String findMostRecentRemoteDisplayableId() {
 204        final boolean multi = mode == Conversation.MODE_MULTI;
 205        synchronized (this.messages) {
 206            for (final Message message : Lists.reverse(this.messages)) {
 207                if (message.getStatus() == Message.STATUS_RECEIVED) {
 208                    final String serverMsgId = message.getServerMsgId();
 209                    if (serverMsgId != null && multi) {
 210                        return serverMsgId;
 211                    }
 212                    return message.getRemoteMsgId();
 213                }
 214            }
 215        }
 216        return null;
 217    }
 218
 219    public int countFailedDeliveries() {
 220        int count = 0;
 221        synchronized (this.messages) {
 222            for(final Message message : this.messages) {
 223                if (message.getStatus() == Message.STATUS_SEND_FAILED) {
 224                    ++count;
 225                }
 226            }
 227        }
 228        return count;
 229    }
 230
 231    public Message getLastEditableMessage() {
 232        synchronized (this.messages) {
 233            for (final Message message : Lists.reverse(this.messages)) {
 234                if (message.isEditable()) {
 235                    if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
 236                        return null;
 237                    }
 238                    return message;
 239                }
 240            }
 241        }
 242        return null;
 243    }
 244
 245
 246    public Message findUnsentMessageWithUuid(String uuid) {
 247        synchronized (this.messages) {
 248            for (final Message message : this.messages) {
 249                final int s = message.getStatus();
 250                if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 251                    return message;
 252                }
 253            }
 254        }
 255        return null;
 256    }
 257
 258    public void findWaitingMessages(OnMessageFound onMessageFound) {
 259        final ArrayList<Message> results = new ArrayList<>();
 260        synchronized (this.messages) {
 261            for (Message message : this.messages) {
 262                if (message.getStatus() == Message.STATUS_WAITING) {
 263                    results.add(message);
 264                }
 265            }
 266        }
 267        for (Message result : results) {
 268            onMessageFound.onMessageFound(result);
 269        }
 270    }
 271
 272    public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) {
 273        final ArrayList<Message> results = new ArrayList<>();
 274        synchronized (this.messages) {
 275            for (final Message message : this.messages) {
 276                if (message.isRead()) {
 277                    continue;
 278                }
 279                results.add(message);
 280            }
 281        }
 282        for (final Message result : results) {
 283            onMessageFound.onMessageFound(result);
 284        }
 285    }
 286
 287    public Message findMessageWithFileAndUuid(final String uuid) {
 288        synchronized (this.messages) {
 289            for (final Message message : this.messages) {
 290                final Transferable transferable = message.getTransferable();
 291                final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
 292                if (message.getUuid().equals(uuid)
 293                        && message.getEncryption() != Message.ENCRYPTION_PGP
 294                        && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
 295                    return message;
 296                }
 297            }
 298        }
 299        return null;
 300    }
 301
 302    public Message findMessageWithUuid(final String uuid) {
 303        synchronized (this.messages) {
 304            for (final Message message : this.messages) {
 305                if (message.getUuid().equals(uuid)) {
 306                    return message;
 307                }
 308            }
 309        }
 310        return null;
 311    }
 312
 313    public boolean markAsDeleted(final List<String> uuids) {
 314        boolean deleted = false;
 315        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 316        synchronized (this.messages) {
 317            for (Message message : this.messages) {
 318                if (uuids.contains(message.getUuid())) {
 319                    message.setDeleted(true);
 320                    deleted = true;
 321                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 322                        pgpDecryptionService.discard(message);
 323                    }
 324                }
 325            }
 326        }
 327        return deleted;
 328    }
 329
 330    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
 331        boolean changed = false;
 332        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 333        synchronized (this.messages) {
 334            for (Message message : this.messages) {
 335                for (final DatabaseBackend.FilePathInfo file : files)
 336                    if (file.uuid.toString().equals(message.getUuid())) {
 337                        message.setDeleted(file.deleted);
 338                        changed = true;
 339                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && 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() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
 368            if (this.mOutgoingChatState != state) {
 369                this.mOutgoingChatState = state;
 370                return true;
 371            }
 372        }
 373        return false;
 374    }
 375
 376    public ChatState getOutgoingChatState() {
 377        return this.mOutgoingChatState;
 378    }
 379
 380    public void trim() {
 381        synchronized (this.messages) {
 382            final int size = messages.size();
 383            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 384            if (size > maxsize) {
 385                List<Message> discards = this.messages.subList(0, size - maxsize);
 386                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 387                if (pgpDecryptionService != null) {
 388                    pgpDecryptionService.discard(discards);
 389                }
 390                discards.clear();
 391                untieMessages();
 392            }
 393        }
 394    }
 395
 396    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 397        final ArrayList<Message> results = new ArrayList<>();
 398        synchronized (this.messages) {
 399            for (Message message : this.messages) {
 400                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
 401                    results.add(message);
 402                }
 403            }
 404        }
 405        for (Message result : results) {
 406            onMessageFound.onMessageFound(result);
 407        }
 408    }
 409
 410    public Message findSentMessageWithUuidOrRemoteId(String id) {
 411        synchronized (this.messages) {
 412            for (Message message : this.messages) {
 413                if (id.equals(message.getUuid())
 414                        || (message.getStatus() >= Message.STATUS_SEND
 415                        && id.equals(message.getRemoteMsgId()))) {
 416                    return message;
 417                }
 418            }
 419        }
 420        return null;
 421    }
 422
 423    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
 424        synchronized (this.messages) {
 425            for (int i = this.messages.size() - 1; i >= 0; --i) {
 426                final Message message = messages.get(i);
 427                final Jid mcp = message.getCounterpart();
 428                if (mcp == null) {
 429                    continue;
 430                }
 431                if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
 432                        && (carbon == message.isCarbon() || received)) {
 433                    final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
 434                    if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
 435                        return message;
 436                    } else {
 437                        return null;
 438                    }
 439                }
 440            }
 441        }
 442        return null;
 443    }
 444
 445    public Message findSentMessageWithUuid(String id) {
 446        synchronized (this.messages) {
 447            for (Message message : this.messages) {
 448                if (id.equals(message.getUuid())) {
 449                    return message;
 450                }
 451            }
 452        }
 453        return null;
 454    }
 455
 456    public Message findMessageWithRemoteId(String id, Jid counterpart) {
 457        synchronized (this.messages) {
 458            for (Message message : this.messages) {
 459                if (counterpart.equals(message.getCounterpart())
 460                        && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
 461                    return message;
 462                }
 463            }
 464        }
 465        return null;
 466    }
 467
 468    public Message findMessageWithServerMsgId(String id) {
 469        synchronized (this.messages) {
 470            for (Message message : this.messages) {
 471                if (id != null && id.equals(message.getServerMsgId())) {
 472                    return message;
 473                }
 474            }
 475        }
 476        return null;
 477    }
 478
 479    public boolean hasMessageWithCounterpart(Jid counterpart) {
 480        synchronized (this.messages) {
 481            for (Message message : this.messages) {
 482                if (counterpart.equals(message.getCounterpart())) {
 483                    return true;
 484                }
 485            }
 486        }
 487        return false;
 488    }
 489
 490    public void populateWithMessages(final List<Message> messages) {
 491        synchronized (this.messages) {
 492            messages.clear();
 493            messages.addAll(this.messages);
 494        }
 495        for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
 496            if (iterator.next().wasMergedIntoPrevious()) {
 497                iterator.remove();
 498            }
 499        }
 500    }
 501
 502    @Override
 503    public boolean isBlocked() {
 504        return getContact().isBlocked();
 505    }
 506
 507    @Override
 508    public boolean isDomainBlocked() {
 509        return getContact().isDomainBlocked();
 510    }
 511
 512    @Override
 513    public Jid getBlockedJid() {
 514        return getContact().getBlockedJid();
 515    }
 516
 517    public int countMessages() {
 518        synchronized (this.messages) {
 519            return this.messages.size();
 520        }
 521    }
 522
 523    public String getFirstMamReference() {
 524        return this.mFirstMamReference;
 525    }
 526
 527    public void setFirstMamReference(String reference) {
 528        this.mFirstMamReference = reference;
 529    }
 530
 531    public void setLastClearHistory(long time, String reference) {
 532        if (reference != null) {
 533            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
 534        } else {
 535            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
 536        }
 537    }
 538
 539    public MamReference getLastClearHistory() {
 540        return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
 541    }
 542
 543    public List<Jid> getAcceptedCryptoTargets() {
 544        if (mode == MODE_SINGLE) {
 545            return Collections.singletonList(getJid().asBareJid());
 546        } else {
 547            return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 548        }
 549    }
 550
 551    public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 552        setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 553    }
 554
 555    public boolean setCorrectingMessage(Message correctingMessage) {
 556        setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
 557        return correctingMessage == null && draftMessage != null;
 558    }
 559
 560    public Message getCorrectingMessage() {
 561        final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
 562        return uuid == null ? null : findSentMessageWithUuid(uuid);
 563    }
 564
 565    public boolean withSelf() {
 566        return getContact().isSelf();
 567    }
 568
 569    @Override
 570    public int compareTo(@NonNull Conversation another) {
 571        return ComparisonChain.start()
 572                .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
 573                .compare(another.getSortableTime(), getSortableTime())
 574                .result();
 575    }
 576
 577    private long getSortableTime() {
 578        Draft draft = getDraft();
 579        long messageTime = getLatestMessage().getTimeSent();
 580        if (draft == null) {
 581            return messageTime;
 582        } else {
 583            return Math.max(messageTime, draft.getTimestamp());
 584        }
 585    }
 586
 587    public String getDraftMessage() {
 588        return draftMessage;
 589    }
 590
 591    public void setDraftMessage(String draftMessage) {
 592        this.draftMessage = draftMessage;
 593    }
 594
 595    public boolean isRead() {
 596        synchronized (this.messages) {
 597            for(final Message message : Lists.reverse(this.messages)) {
 598                if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
 599                    continue;
 600                }
 601                return message.isRead();
 602            }
 603            return true;
 604        }
 605    }
 606
 607    public List<Message> markRead(String upToUuid) {
 608        final List<Message> unread = new ArrayList<>();
 609        synchronized (this.messages) {
 610            for (Message message : this.messages) {
 611                if (!message.isRead()) {
 612                    message.markRead();
 613                    unread.add(message);
 614                }
 615                if (message.getUuid().equals(upToUuid)) {
 616                    return unread;
 617                }
 618            }
 619        }
 620        return unread;
 621    }
 622
 623    public Message getLatestMessage() {
 624        synchronized (this.messages) {
 625            if (this.messages.size() == 0) {
 626                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 627                message.setType(Message.TYPE_STATUS);
 628                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 629                return message;
 630            } else {
 631                return this.messages.get(this.messages.size() - 1);
 632            }
 633        }
 634    }
 635
 636    public @NonNull
 637    CharSequence getName() {
 638        if (getMode() == MODE_MULTI) {
 639            final String roomName = getMucOptions().getName();
 640            final String subject = getMucOptions().getSubject();
 641            final Bookmark bookmark = getBookmark();
 642            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 643            if (printableValue(roomName)) {
 644                return roomName;
 645            } else if (printableValue(subject)) {
 646                return subject;
 647            } else if (printableValue(bookmarkName, false)) {
 648                return bookmarkName;
 649            } else {
 650                final String generatedName = getMucOptions().createNameFromParticipants();
 651                if (printableValue(generatedName)) {
 652                    return generatedName;
 653                } else {
 654                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 655                }
 656            }
 657        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 658            return contactJid;
 659        } else {
 660            return this.getContact().getDisplayName();
 661        }
 662    }
 663
 664    public String getAccountUuid() {
 665        return this.accountUuid;
 666    }
 667
 668    public Account getAccount() {
 669        return this.account;
 670    }
 671
 672    public void setAccount(final Account account) {
 673        this.account = account;
 674    }
 675
 676    public Contact getContact() {
 677        return this.account.getRoster().getContact(this.contactJid);
 678    }
 679
 680    @Override
 681    public Jid getJid() {
 682        return this.contactJid;
 683    }
 684
 685    public int getStatus() {
 686        return this.status;
 687    }
 688
 689    public void setStatus(int status) {
 690        this.status = status;
 691    }
 692
 693    public long getCreated() {
 694        return this.created;
 695    }
 696
 697    public ContentValues getContentValues() {
 698        ContentValues values = new ContentValues();
 699        values.put(UUID, uuid);
 700        values.put(NAME, name);
 701        values.put(CONTACT, contactUuid);
 702        values.put(ACCOUNT, accountUuid);
 703        values.put(CONTACTJID, contactJid.toString());
 704        values.put(CREATED, created);
 705        values.put(STATUS, status);
 706        values.put(MODE, mode);
 707        synchronized (this.attributes) {
 708            values.put(ATTRIBUTES, attributes.toString());
 709        }
 710        return values;
 711    }
 712
 713    public int getMode() {
 714        return this.mode;
 715    }
 716
 717    public void setMode(int mode) {
 718        this.mode = mode;
 719    }
 720
 721    /**
 722     * short for is Private and Non-anonymous
 723     */
 724    public boolean isSingleOrPrivateAndNonAnonymous() {
 725        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 726    }
 727
 728    public boolean isPrivateAndNonAnonymous() {
 729        return getMucOptions().isPrivateAndNonAnonymous();
 730    }
 731
 732    public synchronized MucOptions getMucOptions() {
 733        if (this.mucOptions == null) {
 734            this.mucOptions = new MucOptions(this);
 735        }
 736        return this.mucOptions;
 737    }
 738
 739    public void resetMucOptions() {
 740        this.mucOptions = null;
 741    }
 742
 743    public void setContactJid(final Jid jid) {
 744        this.contactJid = jid;
 745    }
 746
 747    public Jid getNextCounterpart() {
 748        return this.nextCounterpart;
 749    }
 750
 751    public void setNextCounterpart(Jid jid) {
 752        this.nextCounterpart = jid;
 753    }
 754
 755    public int getNextEncryption() {
 756        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 757            return Message.ENCRYPTION_NONE;
 758        }
 759        if (OmemoSetting.isAlways()) {
 760            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 761        }
 762        final int defaultEncryption;
 763        if (suitableForOmemoByDefault(this)) {
 764            defaultEncryption = OmemoSetting.getEncryption();
 765        } else {
 766            defaultEncryption = Message.ENCRYPTION_NONE;
 767        }
 768        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 769        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 770            return defaultEncryption;
 771        } else {
 772            return encryption;
 773        }
 774    }
 775
 776    public boolean setNextEncryption(int encryption) {
 777        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 778    }
 779
 780    public String getNextMessage() {
 781        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 782        return nextMessage == null ? "" : nextMessage;
 783    }
 784
 785    public @Nullable
 786    Draft getDraft() {
 787        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 788        if (timestamp > getLatestMessage().getTimeSent()) {
 789            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 790            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 791                return new Draft(message, timestamp);
 792            }
 793        }
 794        return null;
 795    }
 796
 797    public boolean setNextMessage(final String input) {
 798        final String message = input == null || input.trim().isEmpty() ? null : input;
 799        boolean changed = !getNextMessage().equals(message);
 800        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 801        if (changed) {
 802            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 803        }
 804        return changed;
 805    }
 806
 807    public Bookmark getBookmark() {
 808        return this.account.getBookmark(this.contactJid);
 809    }
 810
 811    public Message findDuplicateMessage(Message message) {
 812        synchronized (this.messages) {
 813            for (int i = this.messages.size() - 1; i >= 0; --i) {
 814                if (this.messages.get(i).similar(message)) {
 815                    return this.messages.get(i);
 816                }
 817            }
 818        }
 819        return null;
 820    }
 821
 822    public boolean hasDuplicateMessage(Message message) {
 823        return findDuplicateMessage(message) != null;
 824    }
 825
 826    public Message findSentMessageWithBody(String body) {
 827        synchronized (this.messages) {
 828            for (int i = this.messages.size() - 1; i >= 0; --i) {
 829                Message message = this.messages.get(i);
 830                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 831                    String otherBody;
 832                    if (message.hasFileOnRemoteHost()) {
 833                        otherBody = message.getFileParams().url;
 834                    } else {
 835                        otherBody = message.body;
 836                    }
 837                    if (otherBody != null && otherBody.equals(body)) {
 838                        return message;
 839                    }
 840                }
 841            }
 842            return null;
 843        }
 844    }
 845
 846    public Message findRtpSession(final String sessionId, final int s) {
 847        synchronized (this.messages) {
 848            for (int i = this.messages.size() - 1; i >= 0; --i) {
 849                final Message message = this.messages.get(i);
 850                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 851                    return message;
 852                }
 853            }
 854        }
 855        return null;
 856    }
 857
 858    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
 859        if (serverMsgId == null || remoteMsgId == null) {
 860            return false;
 861        }
 862        synchronized (this.messages) {
 863            for (Message message : this.messages) {
 864                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
 865                    return true;
 866                }
 867            }
 868        }
 869        return false;
 870    }
 871
 872    public MamReference getLastMessageTransmitted() {
 873        final MamReference lastClear = getLastClearHistory();
 874        MamReference lastReceived = new MamReference(0);
 875        synchronized (this.messages) {
 876            for (int i = this.messages.size() - 1; i >= 0; --i) {
 877                final Message message = this.messages.get(i);
 878                if (message.isPrivateMessage()) {
 879                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 880                }
 881                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 882                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
 883                    break;
 884                }
 885            }
 886        }
 887        return MamReference.max(lastClear, lastReceived);
 888    }
 889
 890    public void setMutedTill(long value) {
 891        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 892    }
 893
 894    public boolean isMuted() {
 895        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 896    }
 897
 898    public boolean alwaysNotify() {
 899        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
 900    }
 901
 902    public boolean setAttribute(String key, boolean value) {
 903        return setAttribute(key, String.valueOf(value));
 904    }
 905
 906    private boolean setAttribute(String key, long value) {
 907        return setAttribute(key, Long.toString(value));
 908    }
 909
 910    private boolean setAttribute(String key, int value) {
 911        return setAttribute(key, String.valueOf(value));
 912    }
 913
 914    public boolean setAttribute(String key, String value) {
 915        synchronized (this.attributes) {
 916            try {
 917                if (value == null) {
 918                    if (this.attributes.has(key)) {
 919                        this.attributes.remove(key);
 920                        return true;
 921                    } else {
 922                        return false;
 923                    }
 924                } else {
 925                    final String prev = this.attributes.optString(key, null);
 926                    this.attributes.put(key, value);
 927                    return !value.equals(prev);
 928                }
 929            } catch (JSONException e) {
 930                throw new AssertionError(e);
 931            }
 932        }
 933    }
 934
 935    public boolean setAttribute(String key, List<Jid> jids) {
 936        JSONArray array = new JSONArray();
 937        for (Jid jid : jids) {
 938            array.put(jid.asBareJid().toString());
 939        }
 940        synchronized (this.attributes) {
 941            try {
 942                this.attributes.put(key, array);
 943                return true;
 944            } catch (JSONException e) {
 945                return false;
 946            }
 947        }
 948    }
 949
 950    public String getAttribute(String key) {
 951        synchronized (this.attributes) {
 952            return this.attributes.optString(key, null);
 953        }
 954    }
 955
 956    private List<Jid> getJidListAttribute(String key) {
 957        ArrayList<Jid> list = new ArrayList<>();
 958        synchronized (this.attributes) {
 959            try {
 960                JSONArray array = this.attributes.getJSONArray(key);
 961                for (int i = 0; i < array.length(); ++i) {
 962                    try {
 963                        list.add(Jid.of(array.getString(i)));
 964                    } catch (IllegalArgumentException e) {
 965                        //ignored
 966                    }
 967                }
 968            } catch (JSONException e) {
 969                //ignored
 970            }
 971        }
 972        return list;
 973    }
 974
 975    private int getIntAttribute(String key, int defaultValue) {
 976        String value = this.getAttribute(key);
 977        if (value == null) {
 978            return defaultValue;
 979        } else {
 980            try {
 981                return Integer.parseInt(value);
 982            } catch (NumberFormatException e) {
 983                return defaultValue;
 984            }
 985        }
 986    }
 987
 988    public long getLongAttribute(String key, long defaultValue) {
 989        String value = this.getAttribute(key);
 990        if (value == null) {
 991            return defaultValue;
 992        } else {
 993            try {
 994                return Long.parseLong(value);
 995            } catch (NumberFormatException e) {
 996                return defaultValue;
 997            }
 998        }
 999    }
1000
1001    public boolean getBooleanAttribute(String key, boolean defaultValue) {
1002        String value = this.getAttribute(key);
1003        if (value == null) {
1004            return defaultValue;
1005        } else {
1006            return Boolean.parseBoolean(value);
1007        }
1008    }
1009
1010    public void add(Message message) {
1011        synchronized (this.messages) {
1012            this.messages.add(message);
1013        }
1014    }
1015
1016    public void prepend(int offset, Message message) {
1017        synchronized (this.messages) {
1018            this.messages.add(Math.min(offset, this.messages.size()), message);
1019        }
1020    }
1021
1022    public void addAll(int index, List<Message> messages) {
1023        synchronized (this.messages) {
1024            this.messages.addAll(index, messages);
1025        }
1026        account.getPgpDecryptionService().decrypt(messages);
1027    }
1028
1029    public void expireOldMessages(long timestamp) {
1030        synchronized (this.messages) {
1031            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
1032                if (iterator.next().getTimeSent() < timestamp) {
1033                    iterator.remove();
1034                }
1035            }
1036            untieMessages();
1037        }
1038    }
1039
1040    public void sort() {
1041        synchronized (this.messages) {
1042            Collections.sort(this.messages, (left, right) -> {
1043                if (left.getTimeSent() < right.getTimeSent()) {
1044                    return -1;
1045                } else if (left.getTimeSent() > right.getTimeSent()) {
1046                    return 1;
1047                } else {
1048                    return 0;
1049                }
1050            });
1051            untieMessages();
1052        }
1053    }
1054
1055    private void untieMessages() {
1056        for (Message message : this.messages) {
1057            message.untie();
1058        }
1059    }
1060
1061    public int unreadCount() {
1062        synchronized (this.messages) {
1063            int count = 0;
1064            for(final Message message : Lists.reverse(this.messages)) {
1065                if (message.isRead()) {
1066                    if (message.getType() == Message.TYPE_RTP_SESSION) {
1067                        continue;
1068                    }
1069                    return count;
1070                }
1071                ++count;
1072            }
1073            return count;
1074        }
1075    }
1076
1077    public int receivedMessagesCount() {
1078        int count = 0;
1079        synchronized (this.messages) {
1080            for (Message message : messages) {
1081                if (message.getStatus() == Message.STATUS_RECEIVED) {
1082                    ++count;
1083                }
1084            }
1085        }
1086        return count;
1087    }
1088
1089    public int sentMessagesCount() {
1090        int count = 0;
1091        synchronized (this.messages) {
1092            for (Message message : messages) {
1093                if (message.getStatus() != Message.STATUS_RECEIVED) {
1094                    ++count;
1095                }
1096            }
1097        }
1098        return count;
1099    }
1100
1101    public boolean isWithStranger() {
1102        final Contact contact = getContact();
1103        return mode == MODE_SINGLE
1104                && !contact.isOwnServer()
1105                && !contact.showInContactList()
1106                && !contact.isSelf()
1107                && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
1108                && sentMessagesCount() == 0;
1109    }
1110
1111    public int getReceivedMessagesCountSinceUuid(String uuid) {
1112        if (uuid == null) {
1113            return 0;
1114        }
1115        int count = 0;
1116        synchronized (this.messages) {
1117            for (int i = messages.size() - 1; i >= 0; i--) {
1118                final Message message = messages.get(i);
1119                if (uuid.equals(message.getUuid())) {
1120                    return count;
1121                }
1122                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1123                    ++count;
1124                }
1125            }
1126        }
1127        return 0;
1128    }
1129
1130    @Override
1131    public int getAvatarBackgroundColor() {
1132        return UIHelper.getColorForName(getName().toString());
1133    }
1134
1135    @Override
1136    public String getAvatarName() {
1137        return getName().toString();
1138    }
1139
1140    public void setCurrentTab(int tab) {
1141        mCurrentTab = tab;
1142    }
1143
1144    public int getCurrentTab() {
1145        if (mCurrentTab >= 0) return mCurrentTab;
1146
1147        if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
1148            return 0;
1149        }
1150
1151        return 1;
1152    }
1153
1154    public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1155        pagerAdapter.startCommand(command, xmppConnectionService);
1156    }
1157
1158    public void setupViewPager(ViewPager pager, TabLayout tabs) {
1159        pagerAdapter.setupViewPager(pager, tabs);
1160    }
1161
1162    public interface OnMessageFound {
1163        void onMessageFound(final Message message);
1164    }
1165
1166    public static class Draft {
1167        private final String message;
1168        private final long timestamp;
1169
1170        private Draft(String message, long timestamp) {
1171            this.message = message;
1172            this.timestamp = timestamp;
1173        }
1174
1175        public long getTimestamp() {
1176            return timestamp;
1177        }
1178
1179        public String getMessage() {
1180            return message;
1181        }
1182    }
1183
1184    public class ConversationPagerAdapter extends PagerAdapter {
1185        protected ViewPager mPager = null;
1186        protected TabLayout mTabs = null;
1187        ArrayList<CommandSession> sessions = new ArrayList<>();
1188
1189        public void setupViewPager(ViewPager pager, TabLayout tabs) {
1190            mPager = pager;
1191            mTabs = tabs;
1192            pager.setAdapter(this);
1193            tabs.setupWithViewPager(mPager);
1194            pager.setCurrentItem(getCurrentTab());
1195
1196            mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1197                public void onPageScrollStateChanged(int state) { }
1198                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1199
1200                public void onPageSelected(int position) {
1201                    setCurrentTab(position);
1202                }
1203            });
1204        }
1205
1206        public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1207            CommandSession session = new CommandSession(command.getAttribute("name"), xmppConnectionService);
1208
1209            final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1210            packet.setTo(command.getAttributeAsJid("jid"));
1211            final Element c = packet.addChild("command", Namespace.COMMANDS);
1212            c.setAttribute("node", command.getAttribute("node"));
1213            c.setAttribute("action", "execute");
1214            xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1215                mPager.post(() -> {
1216                    session.updateWithResponse(iq);
1217                });
1218            });
1219
1220            sessions.add(session);
1221            notifyDataSetChanged();
1222            mPager.setCurrentItem(getCount() - 1);
1223        }
1224
1225        @NonNull
1226        @Override
1227        public Object instantiateItem(@NonNull ViewGroup container, int position) {
1228            if (position < 2) {
1229              return mPager.getChildAt(position);
1230            }
1231
1232            CommandSession session = sessions.get(position-2);
1233            CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
1234            container.addView(binding.getRoot());
1235            binding.form.setAdapter(session);
1236            binding.done.setOnClickListener((button) -> {
1237                if (session.execute()) {
1238                    sessions.remove(session);
1239                    notifyDataSetChanged();
1240                }
1241            });
1242
1243            session.setBinding(binding);
1244            return session;
1245        }
1246
1247        @Override
1248        public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1249            if (position < 2) return;
1250
1251            container.removeView(((CommandSession) o).getView());
1252        }
1253
1254        @Override
1255        public int getItemPosition(Object o) {
1256            if (o == mPager.getChildAt(0)) return PagerAdapter.POSITION_UNCHANGED;
1257            if (o == mPager.getChildAt(1)) return PagerAdapter.POSITION_UNCHANGED;
1258
1259            int pos = sessions.indexOf(o);
1260            if (pos < 0) return PagerAdapter.POSITION_NONE;
1261            return pos + 2;
1262        }
1263
1264        @Override
1265        public int getCount() {
1266            int count = 2 + sessions.size();
1267            if (count > 2) {
1268                mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1269            } else {
1270                mTabs.setTabMode(TabLayout.MODE_FIXED);
1271            }
1272            return count;
1273        }
1274
1275        @Override
1276        public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1277            if (view == o) return true;
1278
1279            if (o instanceof CommandSession) {
1280                return ((CommandSession) o).getView() == view;
1281            }
1282
1283            return false;
1284        }
1285
1286        @Nullable
1287        @Override
1288        public CharSequence getPageTitle(int position) {
1289            switch (position) {
1290                case 0:
1291                    return "Conversation";
1292                case 1:
1293                    return "Commands";
1294                default:
1295                    CommandSession session = sessions.get(position-2);
1296                    if (session == null) return super.getPageTitle(position);
1297                    return session.getTitle();
1298            }
1299        }
1300
1301        class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
1302            abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1303                protected T binding;
1304
1305                public ViewHolder(T binding) {
1306                    super(binding.getRoot());
1307                    this.binding = binding;
1308                }
1309
1310                abstract public void bind(Element el);
1311            }
1312
1313            class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1314                public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1315
1316                @Override
1317                public void bind(Element iq) {
1318                    binding.errorIcon.setVisibility(View.VISIBLE);
1319
1320                    Element error = iq.findChild("error");
1321                    if (error == null) return;
1322                    String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1323                    if (text == null || text.equals("")) {
1324                        text = error.getChildren().get(0).getName();
1325                    }
1326                    binding.message.setText(text);
1327                }
1328            }
1329
1330            class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1331                public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1332
1333                @Override
1334                public void bind(Element note) {
1335                    binding.message.setText(note.getContent());
1336
1337                    String type = note.getAttribute("type");
1338                    if (type != null && type.equals("error")) {
1339                        binding.errorIcon.setVisibility(View.VISIBLE);
1340                    }
1341                }
1342            }
1343
1344            class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1345                public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1346
1347                @Override
1348                public void bind(Element field) {
1349                    String label = field.getAttribute("label");
1350                    if (label == null) label = field.getAttribute("var");
1351                    if (label == null) {
1352                        binding.label.setVisibility(View.GONE);
1353                    } else {
1354                        binding.label.setVisibility(View.VISIBLE);
1355                        binding.label.setText(label);
1356                    }
1357
1358                    String desc = field.findChildContent("desc", "jabber:x:data");
1359                    if (desc == null) {
1360                        binding.desc.setVisibility(View.GONE);
1361                    } else {
1362                        binding.desc.setVisibility(View.VISIBLE);
1363                        binding.desc.setText(desc);
1364                    }
1365
1366                    ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
1367                    for (Element el : field.getChildren()) {
1368                        if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1369                            values.add(el.getContent());
1370                        }
1371                    }
1372                    binding.values.setAdapter(values);
1373                }
1374            }
1375
1376            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
1377                public TextFieldViewHolder(CommandTextFieldBinding binding) {
1378                    super(binding);
1379                    binding.textinput.addTextChangedListener(this);
1380                }
1381                protected Element mValue = null;
1382
1383                @Override
1384                public void bind(Element field) {
1385                    String label = field.getAttribute("label");
1386                    if (label == null) label = field.getAttribute("var");
1387                    if (label == null) label = "";
1388                    binding.textinputLayout.setHint(label);
1389
1390                    String desc = field.findChildContent("desc", "jabber:x:data");
1391                    if (desc == null) {
1392                        binding.desc.setVisibility(View.GONE);
1393                    } else {
1394                        binding.desc.setVisibility(View.VISIBLE);
1395                        binding.desc.setText(desc);
1396                    }
1397
1398                    mValue = field.findChild("value", "jabber:x:data");
1399                    if (mValue == null) {
1400                        mValue = field.addChild("value", "jabber:x:data");
1401                    }
1402                    binding.textinput.setText(mValue.getContent());
1403                }
1404
1405                @Override
1406                public void afterTextChanged(Editable s) {
1407                    if (mValue == null) return;
1408
1409                    mValue.setContent(s.toString());
1410                }
1411
1412                @Override
1413                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1414
1415                @Override
1416                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1417            }
1418
1419            class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
1420                public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
1421
1422                @Override
1423                public void bind(Element oob) {
1424                    binding.webview.getSettings().setJavaScriptEnabled(true);
1425                    binding.webview.setWebViewClient(new WebViewClient() {
1426                        @Override
1427                        public void onPageFinished(WebView view, String url) {
1428                            super.onPageFinished(view, url);
1429                            mTitle = view.getTitle();
1430                            ConversationPagerAdapter.this.notifyDataSetChanged();
1431                        }
1432                    });
1433                    binding.webview.loadUrl(oob.findChildContent("url", "jabber:x:oob"));
1434                }
1435            }
1436
1437            final int TYPE_ERROR = 1;
1438            final int TYPE_NOTE = 2;
1439            final int TYPE_WEB = 3;
1440            final int TYPE_RESULT_FIELD = 4;
1441            final int TYPE_TEXT_FIELD = 5;
1442
1443            protected String mTitle;
1444            protected CommandPageBinding mBinding = null;
1445            protected IqPacket response = null;
1446            protected Element responseElement = null;
1447            protected XmppConnectionService xmppConnectionService;
1448
1449            CommandSession(String title, XmppConnectionService xmppConnectionService) {
1450                mTitle = title;
1451                this.xmppConnectionService = xmppConnectionService;
1452            }
1453
1454            public String getTitle() {
1455                return mTitle;
1456            }
1457
1458            public void updateWithResponse(IqPacket iq) {
1459                this.responseElement = null;
1460                this.response = iq;
1461
1462                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1463                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1464                    for (Element el : command.getChildren()) {
1465                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1466                            String title = el.findChildContent("title", "jabber:x:data");
1467                            if (title != null) {
1468                                mTitle = title;
1469                                ConversationPagerAdapter.this.notifyDataSetChanged();
1470                            }
1471                            this.responseElement = el;
1472                            break;
1473                        }
1474                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1475                            String url = el.findChildContent("url", "jabber:x:oob");
1476                            if (url != null) {
1477                                String scheme = Uri.parse(url).getScheme();
1478                                if (scheme.equals("http") || scheme.equals("https")) {
1479                                    this.responseElement = el;
1480                                    break;
1481                                }
1482                            }
1483                        }
1484                        if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1485                            this.responseElement = el;
1486                            break;
1487                        }
1488                    }
1489                }
1490
1491                notifyDataSetChanged();
1492            }
1493
1494            @Override
1495            public int getItemCount() {
1496                if (response == null) return 0;
1497                if (response.getType() == IqPacket.TYPE.RESULT && responseElement.getNamespace().equals("jabber:x:data")) {
1498                    int i = 0;
1499                    for (Element el : responseElement.getChildren()) {
1500                        if (!el.getNamespace().equals("jabber:x:data")) continue;
1501                        if (el.getName().equals("title")) continue;
1502                        if (el.getName().equals("field")) {
1503                            String type = el.getAttribute("type");
1504                            if (type != null && type.equals("hidden")) continue;
1505                        }
1506
1507                        i++;
1508                    }
1509                    return i;
1510                }
1511                return 1;
1512            }
1513
1514            public Element getItem(int position) {
1515                if (response == null) return null;
1516
1517                if (response.getType() == IqPacket.TYPE.RESULT) {
1518                    if (responseElement.getNamespace().equals("jabber:x:data")) {
1519                        int i = 0;
1520                        for (Element el : responseElement.getChildren()) {
1521                            if (!el.getNamespace().equals("jabber:x:data")) continue;
1522                            if (el.getName().equals("title")) continue;
1523                            if (el.getName().equals("field")) {
1524                                String type = el.getAttribute("type");
1525                                if (type != null && type.equals("hidden")) continue;
1526                            }
1527
1528                            if (i < position) {
1529                                i++;
1530                                continue;
1531                            }
1532
1533                            return el;
1534                        }
1535                    }
1536                }
1537
1538                return responseElement == null ? response : responseElement;
1539            }
1540
1541            @Override
1542            public int getItemViewType(int position) {
1543                if (response == null) return -1;
1544
1545                if (response.getType() == IqPacket.TYPE.RESULT) {
1546                    Element item = getItem(position);
1547                    if (item.getName().equals("note")) return TYPE_NOTE;
1548                    if (item.getNamespace().equals("jabber:x:oob")) return TYPE_WEB;
1549                    if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) return TYPE_NOTE;
1550                    if (item.getName().equals("field") && item.getNamespace().equals("jabber:x:data")) {
1551                        String formType = responseElement.getAttribute("type");
1552                        String fieldType = item.getAttribute("type");
1553                        if (formType.equals("result") || fieldType.equals("fixed")) return TYPE_RESULT_FIELD;
1554                        if (formType.equals("form")) return TYPE_TEXT_FIELD;
1555                    }
1556                    return -1;
1557                } else {
1558                    return TYPE_ERROR;
1559                }
1560            }
1561
1562            @Override
1563            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
1564                switch(viewType) {
1565                    case TYPE_ERROR: {
1566                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1567                        return new ErrorViewHolder(binding);
1568                    }
1569                    case TYPE_NOTE: {
1570                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1571                        return new NoteViewHolder(binding);
1572                    }
1573                    case TYPE_WEB: {
1574                        CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
1575                        return new WebViewHolder(binding);
1576                    }
1577                    case TYPE_RESULT_FIELD: {
1578                        CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
1579                        return new ResultFieldViewHolder(binding);
1580                    }
1581                    case TYPE_TEXT_FIELD: {
1582                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
1583                        return new TextFieldViewHolder(binding);
1584                    }
1585                    default:
1586                        throw new IllegalArgumentException("Unknown viewType: " + viewType);
1587                }
1588            }
1589
1590            @Override
1591            public void onBindViewHolder(ViewHolder viewHolder, int position) {
1592                viewHolder.bind(getItem(position));
1593            }
1594
1595            public View getView() {
1596                return mBinding.getRoot();
1597            }
1598
1599            public boolean execute() {
1600                if (response == null || responseElement == null) return true;
1601                Element command = response.findChild("command", "http://jabber.org/protocol/commands");
1602                if (command == null) return true;
1603                String status = command.getAttribute("status");
1604                if (status == null || !status.equals("executing")) return true;
1605                if (!responseElement.getName().equals("x") || !responseElement.getNamespace().equals("jabber:x:data")) return true;
1606                String formType = responseElement.getAttribute("type");
1607                if (formType == null || !formType.equals("form")) return true;
1608
1609                responseElement.setAttribute("type", "submit");
1610
1611                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1612                packet.setTo(response.getFrom());
1613                final Element c = packet.addChild("command", Namespace.COMMANDS);
1614                c.setAttribute("node", command.getAttribute("node"));
1615                c.setAttribute("sessionid", command.getAttribute("sessionid"));
1616                c.setAttribute("action", "execute");
1617                c.addChild(responseElement);
1618
1619                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1620                    getView().post(() -> {
1621                        updateWithResponse(iq);
1622                    });
1623                });
1624
1625                return false;
1626            }
1627
1628            public void setBinding(CommandPageBinding b) {
1629                mBinding = b;
1630                mBinding.form.setLayoutManager(new LinearLayoutManager(mPager.getContext()) {
1631                    @Override
1632                    public boolean canScrollVertically() { return getItemCount() > 1; }
1633                });
1634            }
1635        }
1636    }
1637}