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