Conversation.java

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