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