Conversation.java

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