Conversation.java

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