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.getSettings().setUserAgentString("Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36");
1686                    binding.webview.getSettings().setDatabaseEnabled(true);
1687                    binding.webview.getSettings().setDomStorageEnabled(true);
1688                    binding.webview.setWebViewClient(new WebViewClient() {
1689                        @Override
1690                        public void onPageFinished(WebView view, String url) {
1691                            super.onPageFinished(view, url);
1692                            mTitle = view.getTitle();
1693                            ConversationPagerAdapter.this.notifyDataSetChanged();
1694                        }
1695                    });
1696                    binding.webview.loadUrl(oob.el.findChildContent("url", "jabber:x:oob"));
1697                }
1698            }
1699
1700            class Item {
1701                protected Element el;
1702                protected int viewType;
1703                protected String error = null;
1704
1705                Item(Element el, int viewType) {
1706                    this.el = el;
1707                    this.viewType = viewType;
1708                }
1709
1710                public boolean validate() {
1711                    error = null;
1712                    return true;
1713                }
1714            }
1715
1716            class Field extends Item {
1717                Field(Element el, int viewType) { super(el, viewType); }
1718
1719                @Override
1720                public boolean validate() {
1721                    if (!super.validate()) return false;
1722                    if (el.findChild("required", "jabber:x:data") == null) return true;
1723                    if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
1724
1725                    error = "this value is required";
1726                    return false;
1727                }
1728
1729                public Optional<String> getLabel() {
1730                    String label = el.getAttribute("label");
1731                    if (label == null) label = el.getAttribute("var");
1732                    return Optional.ofNullable(label);
1733                }
1734
1735                public Optional<String> getDesc() {
1736                    return Optional.ofNullable(el.findChildContent("desc", "jabber:x:data"));
1737                }
1738
1739                public Element getValue() {
1740                    Element value = el.findChild("value", "jabber:x:data");
1741                    if (value == null) {
1742                        value = el.addChild("value", "jabber:x:data");
1743                    }
1744                    return value;
1745                }
1746
1747                public List<Option> getOptions() {
1748                    return Option.forField(el);
1749                }
1750            }
1751
1752            class Cell extends Item {
1753                protected Element reported;
1754
1755                Cell(Element reported, Element item) {
1756                    super(item, TYPE_RESULT_CELL);
1757                    this.reported = reported;
1758                }
1759            }
1760
1761            protected Item mkItem(Element el, int pos) {
1762                int viewType = -1;
1763
1764                if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
1765                    if (el.getName().equals("note")) {
1766                        viewType = TYPE_NOTE;
1767                    } else if (el.getNamespace().equals("jabber:x:oob")) {
1768                        viewType = TYPE_WEB;
1769                    } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
1770                        viewType = TYPE_NOTE;
1771                    } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
1772                        String formType = responseElement.getAttribute("type");
1773                        if (formType != null) {
1774                            String fieldType = el.getAttribute("type");
1775                            if (fieldType == null) fieldType = "text-single";
1776
1777                            if (formType.equals("result") || fieldType.equals("fixed")) {
1778                                viewType = TYPE_RESULT_FIELD;
1779                            } else if (formType.equals("form")) {
1780                                if (fieldType.equals("boolean")) {
1781                                    viewType = TYPE_CHECKBOX_FIELD;
1782                                } else if (fieldType.equals("list-single")) {
1783                                    Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1784                                    if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
1785                                        viewType = TYPE_RADIO_EDIT_FIELD;
1786                                    } else {
1787                                        viewType = TYPE_SPINNER_FIELD;
1788                                    }
1789                                } else {
1790                                    viewType = TYPE_TEXT_FIELD;
1791                                }
1792                            }
1793
1794                            Field field = new Field(el, viewType);
1795                            items.put(pos, field);
1796                            return field;
1797                        }
1798                    }
1799                } else if (response != null) {
1800                    viewType = TYPE_ERROR;
1801                }
1802
1803                Item item = new Item(el, viewType);
1804                items.put(pos, item);
1805                return item;
1806            }
1807
1808            final int TYPE_ERROR = 1;
1809            final int TYPE_NOTE = 2;
1810            final int TYPE_WEB = 3;
1811            final int TYPE_RESULT_FIELD = 4;
1812            final int TYPE_TEXT_FIELD = 5;
1813            final int TYPE_CHECKBOX_FIELD = 6;
1814            final int TYPE_SPINNER_FIELD = 7;
1815            final int TYPE_RADIO_EDIT_FIELD = 8;
1816            final int TYPE_RESULT_CELL = 9;
1817
1818            protected String mTitle;
1819            protected CommandPageBinding mBinding = null;
1820            protected IqPacket response = null;
1821            protected Element responseElement = null;
1822            protected Element reported = null;
1823            protected SparseArray<Item> items = new SparseArray<>();
1824            protected XmppConnectionService xmppConnectionService;
1825            protected ArrayAdapter<String> actionsAdapter;
1826            protected GridLayoutManager layoutManager;
1827
1828            CommandSession(String title, XmppConnectionService xmppConnectionService) {
1829                mTitle = title;
1830                this.xmppConnectionService = xmppConnectionService;
1831                setupLayoutManager();
1832                actionsAdapter = new ArrayAdapter<String>(xmppConnectionService, R.layout.simple_list_item) {
1833                    @Override
1834                    public View getView(int position, View convertView, ViewGroup parent) {
1835                        View v = super.getView(position, convertView, parent);
1836                        TextView tv = (TextView) v.findViewById(android.R.id.text1);
1837                        tv.setGravity(Gravity.CENTER);
1838                        int resId = xmppConnectionService.getResources().getIdentifier("action_" + tv.getText() , "string" , xmppConnectionService.getPackageName());
1839                        if (resId != 0) tv.setText(xmppConnectionService.getResources().getString(resId));
1840                        tv.setTextColor(ContextCompat.getColor(xmppConnectionService, R.color.white));
1841                        tv.setBackgroundColor(UIHelper.getColorForName(tv.getText().toString()));
1842                        return v;
1843                    }
1844                };
1845                actionsAdapter.registerDataSetObserver(new DataSetObserver() {
1846                    @Override
1847                    public void onChanged() {
1848                        if (mBinding == null) return;
1849
1850                        mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
1851                    }
1852
1853                    @Override
1854                    public void onInvalidated() {}
1855                });
1856            }
1857
1858            public String getTitle() {
1859                return mTitle;
1860            }
1861
1862            public void updateWithResponse(IqPacket iq) {
1863                this.responseElement = null;
1864                this.reported = null;
1865                this.response = iq;
1866                this.items.clear();
1867                this.actionsAdapter.clear();
1868                layoutManager.setSpanCount(1);
1869
1870                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1871                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1872                    for (Element el : command.getChildren()) {
1873                        if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1874                            for (Element action : el.getChildren()) {
1875                                if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
1876                                if (action.getName().equals("execute")) continue;
1877
1878                                actionsAdapter.add(action.getName());
1879                            }
1880                        }
1881                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1882                            String title = el.findChildContent("title", "jabber:x:data");
1883                            if (title != null) {
1884                                mTitle = title;
1885                                ConversationPagerAdapter.this.notifyDataSetChanged();
1886                            }
1887
1888                            if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
1889                                this.responseElement = el;
1890                                this.reported = el.findChild("reported", "jabber:x:data");
1891                                layoutManager.setSpanCount(this.reported == null ? 1 : this.reported.getChildren().size());
1892                            }
1893                            break;
1894                        }
1895                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1896                            String url = el.findChildContent("url", "jabber:x:oob");
1897                            if (url != null) {
1898                                String scheme = Uri.parse(url).getScheme();
1899                                if (scheme.equals("http") || scheme.equals("https")) {
1900                                    this.responseElement = el;
1901                                    break;
1902                                }
1903                            }
1904                        }
1905                        if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1906                            this.responseElement = el;
1907                            break;
1908                        }
1909                    }
1910
1911                    if (responseElement == null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
1912                        removeSession(this);
1913                        return;
1914                    }
1915
1916                    if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
1917                        // No actions have been given, but we are not done?
1918                        // This is probably a spec violation, but we should do *something*
1919                        actionsAdapter.add("execute");
1920                    }
1921                }
1922
1923                if (actionsAdapter.getCount() > 0) {
1924                    if (actionsAdapter.getPosition("cancel") < 0) actionsAdapter.insert("cancel", 0);
1925                } else {
1926                    actionsAdapter.add("close");
1927                }
1928
1929                notifyDataSetChanged();
1930            }
1931
1932            @Override
1933            public int getItemCount() {
1934                if (response == null) return 0;
1935                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
1936                    int i = 0;
1937                    for (Element el : responseElement.getChildren()) {
1938                        if (!el.getNamespace().equals("jabber:x:data")) continue;
1939                        if (el.getName().equals("title")) continue;
1940                        if (el.getName().equals("field")) {
1941                            String type = el.getAttribute("type");
1942                            if (type != null && type.equals("hidden")) continue;
1943                        }
1944
1945                        if (el.getName().equals("reported") || el.getName().equals("item")) {
1946                            i += el.getChildren().size();
1947                            continue;
1948                        }
1949
1950                        i++;
1951                    }
1952                    return i;
1953                }
1954                return 1;
1955            }
1956
1957            public Item getItem(int position) {
1958                if (items.get(position) != null) return items.get(position);
1959                if (response == null) return null;
1960
1961                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
1962                    if (responseElement.getNamespace().equals("jabber:x:data")) {
1963                        int i = 0;
1964                        for (Element el : responseElement.getChildren()) {
1965                            if (!el.getNamespace().equals("jabber:x:data")) continue;
1966                            if (el.getName().equals("title")) continue;
1967                            if (el.getName().equals("field")) {
1968                                String type = el.getAttribute("type");
1969                                if (type != null && type.equals("hidden")) continue;
1970                            }
1971
1972                            if (el.getName().equals("reported") || el.getName().equals("item")) {
1973                                int col = 0;
1974                                for (Element subel : el.getChildren()) {
1975                                    if (i < position) {
1976                                        i++;
1977                                        col++;
1978                                        continue;
1979                                    }
1980
1981                                    Element reportedField = null;
1982                                    if (reported != null) {
1983                                        int rCol = 0;
1984                                        for (Element field : reported.getChildren()) {
1985                                            if (!field.getName().equals("field") || !field.getNamespace().equals("jabber:x:data")) continue;
1986                                            if (rCol < col) {
1987                                                rCol++;
1988                                                continue;
1989                                            }
1990                                            reportedField = field;
1991                                            break;
1992                                        }
1993                                    }
1994                                    Cell cell = new Cell(reportedField, el.getName().equals("item") ? subel : null);
1995                                    items.put(position, cell);
1996                                    return cell;
1997                                }
1998
1999                                i--;
2000                            }
2001
2002                            if (i < position) {
2003                                i++;
2004                                continue;
2005                            }
2006
2007                            return mkItem(el, position);
2008                        }
2009                    }
2010                }
2011
2012                return mkItem(responseElement == null ? response : responseElement, position);
2013            }
2014
2015            @Override
2016            public int getItemViewType(int position) {
2017                return getItem(position).viewType;
2018            }
2019
2020            @Override
2021            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2022                switch(viewType) {
2023                    case TYPE_ERROR: {
2024                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2025                        return new ErrorViewHolder(binding);
2026                    }
2027                    case TYPE_NOTE: {
2028                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2029                        return new NoteViewHolder(binding);
2030                    }
2031                    case TYPE_WEB: {
2032                        CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2033                        return new WebViewHolder(binding);
2034                    }
2035                    case TYPE_RESULT_FIELD: {
2036                        CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2037                        return new ResultFieldViewHolder(binding);
2038                    }
2039                    case TYPE_RESULT_CELL: {
2040                        CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2041                        return new ResultCellViewHolder(binding);
2042                    }
2043                    case TYPE_CHECKBOX_FIELD: {
2044                        CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2045                        return new CheckboxFieldViewHolder(binding);
2046                    }
2047                    case TYPE_RADIO_EDIT_FIELD: {
2048                        CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2049                        return new RadioEditFieldViewHolder(binding);
2050                    }
2051                    case TYPE_SPINNER_FIELD: {
2052                        CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2053                        return new SpinnerFieldViewHolder(binding);
2054                    }
2055                    case TYPE_TEXT_FIELD: {
2056                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2057                        return new TextFieldViewHolder(binding);
2058                    }
2059                    default:
2060                        throw new IllegalArgumentException("Unknown viewType: " + viewType);
2061                }
2062            }
2063
2064            @Override
2065            public void onBindViewHolder(ViewHolder viewHolder, int position) {
2066                viewHolder.bind(getItem(position));
2067            }
2068
2069            public View getView() {
2070                return mBinding.getRoot();
2071            }
2072
2073            public boolean validate() {
2074                int count = getItemCount();
2075                boolean isValid = true;
2076                for (int i = 0; i < count; i++) {
2077                    boolean oneIsValid = getItem(i).validate();
2078                    isValid = isValid && oneIsValid;
2079                }
2080                notifyDataSetChanged();
2081                return isValid;
2082            }
2083
2084            public boolean execute() {
2085                return execute("execute");
2086            }
2087
2088            public boolean execute(int actionPosition) {
2089                return execute(actionsAdapter.getItem(actionPosition));
2090            }
2091
2092            public boolean execute(String action) {
2093                if (!action.equals("cancel") && !validate()) return false;
2094                if (response == null) return true;
2095                Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2096                if (command == null) return true;
2097                String status = command.getAttribute("status");
2098                if (status == null || !status.equals("executing")) return true;
2099
2100                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2101                packet.setTo(response.getFrom());
2102                final Element c = packet.addChild("command", Namespace.COMMANDS);
2103                c.setAttribute("node", command.getAttribute("node"));
2104                c.setAttribute("sessionid", command.getAttribute("sessionid"));
2105                c.setAttribute("action", action);
2106
2107                String formType = responseElement == null ? null : responseElement.getAttribute("type");
2108                if (!action.equals("cancel") &&
2109                    responseElement != null &&
2110                    responseElement.getName().equals("x") &&
2111                    responseElement.getNamespace().equals("jabber:x:data") &&
2112                    formType != null && formType.equals("form")) {
2113
2114                    responseElement.setAttribute("type", "submit");
2115                    c.addChild(responseElement);
2116                }
2117
2118                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2119                    getView().post(() -> {
2120                        updateWithResponse(iq);
2121                    });
2122                });
2123
2124                return false;
2125            }
2126
2127            protected GridLayoutManager setupLayoutManager() {
2128                layoutManager = new GridLayoutManager(mPager.getContext(), layoutManager == null ? 1 : layoutManager.getSpanCount()) {
2129                    @Override
2130                    public boolean canScrollVertically() { return getItemCount() > 1; }
2131                };
2132                layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2133                    @Override
2134                    public int getSpanSize(int position) {
2135                        if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2136                        return 1;
2137                    }
2138                });
2139                return layoutManager;
2140            }
2141
2142            public void setBinding(CommandPageBinding b) {
2143                mBinding = b;
2144                mBinding.form.setLayoutManager(setupLayoutManager());
2145                mBinding.form.setAdapter(this);
2146                mBinding.actions.setAdapter(actionsAdapter);
2147                mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2148                    if (execute(pos)) {
2149                        removeSession(CommandSession.this);
2150                    }
2151                });
2152
2153                actionsAdapter.notifyDataSetChanged();
2154            }
2155        }
2156    }
2157}