Conversation.java

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