Conversation.java

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