Conversation.java

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