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