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