Conversation.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.content.Context;
   5import android.content.DialogInterface;
   6import android.content.Intent;
   7import android.database.Cursor;
   8import android.database.DataSetObserver;
   9import android.graphics.drawable.BitmapDrawable;
  10import android.graphics.Bitmap;
  11import android.graphics.Canvas;
  12import android.graphics.Rect;
  13import android.net.Uri;
  14import android.text.Editable;
  15import android.text.InputType;
  16import android.text.SpannableStringBuilder;
  17import android.text.Spanned;
  18import android.text.StaticLayout;
  19import android.text.TextPaint;
  20import android.text.TextUtils;
  21import android.text.TextWatcher;
  22import android.view.LayoutInflater;
  23import android.view.MotionEvent;
  24import android.view.Gravity;
  25import android.view.View;
  26import android.view.ViewGroup;
  27import android.widget.AbsListView;
  28import android.widget.ArrayAdapter;
  29import android.widget.AdapterView;
  30import android.widget.Button;
  31import android.widget.CompoundButton;
  32import android.widget.GridLayout;
  33import android.widget.ListView;
  34import android.widget.TextView;
  35import android.widget.Toast;
  36import android.widget.Spinner;
  37import android.webkit.JavascriptInterface;
  38import android.webkit.WebMessage;
  39import android.webkit.WebView;
  40import android.webkit.WebViewClient;
  41import android.webkit.WebChromeClient;
  42import android.util.DisplayMetrics;
  43import android.util.Pair;
  44import android.util.SparseArray;
  45
  46import androidx.annotation.NonNull;
  47import androidx.annotation.Nullable;
  48import androidx.appcompat.app.AlertDialog;
  49import androidx.appcompat.app.AlertDialog.Builder;
  50import androidx.core.content.ContextCompat;
  51import androidx.databinding.DataBindingUtil;
  52import androidx.databinding.ViewDataBinding;
  53import androidx.viewpager.widget.PagerAdapter;
  54import androidx.recyclerview.widget.RecyclerView;
  55import androidx.recyclerview.widget.GridLayoutManager;
  56import androidx.viewpager.widget.ViewPager;
  57
  58import com.caverock.androidsvg.SVG;
  59
  60import com.google.android.material.tabs.TabLayout;
  61import com.google.android.material.textfield.TextInputLayout;
  62import com.google.common.base.Optional;
  63import com.google.common.collect.ComparisonChain;
  64import com.google.common.collect.Lists;
  65
  66import org.json.JSONArray;
  67import org.json.JSONException;
  68import org.json.JSONObject;
  69
  70import java.util.ArrayList;
  71import java.util.Collections;
  72import java.util.Iterator;
  73import java.util.HashSet;
  74import java.util.List;
  75import java.util.ListIterator;
  76import java.util.concurrent.atomic.AtomicBoolean;
  77import java.util.stream.Collectors;
  78import java.util.Set;
  79import java.util.Timer;
  80import java.util.TimerTask;
  81
  82import me.saket.bettermovementmethod.BetterLinkMovementMethod;
  83
  84import eu.siacs.conversations.Config;
  85import eu.siacs.conversations.R;
  86import eu.siacs.conversations.crypto.OmemoSetting;
  87import eu.siacs.conversations.crypto.PgpDecryptionService;
  88import eu.siacs.conversations.databinding.CommandButtonGridFieldBinding;
  89import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
  90import eu.siacs.conversations.databinding.CommandItemCardBinding;
  91import eu.siacs.conversations.databinding.CommandNoteBinding;
  92import eu.siacs.conversations.databinding.CommandPageBinding;
  93import eu.siacs.conversations.databinding.CommandProgressBarBinding;
  94import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
  95import eu.siacs.conversations.databinding.CommandResultCellBinding;
  96import eu.siacs.conversations.databinding.CommandResultFieldBinding;
  97import eu.siacs.conversations.databinding.CommandSearchListFieldBinding;
  98import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
  99import eu.siacs.conversations.databinding.CommandTextFieldBinding;
 100import eu.siacs.conversations.databinding.CommandWebviewBinding;
 101import eu.siacs.conversations.databinding.DialogQuickeditBinding;
 102import eu.siacs.conversations.persistance.DatabaseBackend;
 103import eu.siacs.conversations.services.AvatarService;
 104import eu.siacs.conversations.services.QuickConversationsService;
 105import eu.siacs.conversations.services.XmppConnectionService;
 106import eu.siacs.conversations.ui.UriHandlerActivity;
 107import eu.siacs.conversations.ui.text.FixedURLSpan;
 108import eu.siacs.conversations.ui.util.ShareUtil;
 109import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 110import eu.siacs.conversations.utils.JidHelper;
 111import eu.siacs.conversations.utils.MessageUtils;
 112import eu.siacs.conversations.utils.UIHelper;
 113import eu.siacs.conversations.xml.Element;
 114import eu.siacs.conversations.xml.Namespace;
 115import eu.siacs.conversations.xmpp.Jid;
 116import eu.siacs.conversations.xmpp.chatstate.ChatState;
 117import eu.siacs.conversations.xmpp.forms.Data;
 118import eu.siacs.conversations.xmpp.forms.Option;
 119import eu.siacs.conversations.xmpp.mam.MamReference;
 120import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 121
 122import static eu.siacs.conversations.entities.Bookmark.printableValue;
 123
 124
 125public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
 126    public static final String TABLENAME = "conversations";
 127
 128    public static final int STATUS_AVAILABLE = 0;
 129    public static final int STATUS_ARCHIVED = 1;
 130
 131    public static final String NAME = "name";
 132    public static final String ACCOUNT = "accountUuid";
 133    public static final String CONTACT = "contactUuid";
 134    public static final String CONTACTJID = "contactJid";
 135    public static final String STATUS = "status";
 136    public static final String CREATED = "created";
 137    public static final String MODE = "mode";
 138    public static final String ATTRIBUTES = "attributes";
 139
 140    public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 141    public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
 142    public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
 143    public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
 144    public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
 145    static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 146    static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
 147    static final String ATTRIBUTE_MODERATED = "moderated";
 148    static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
 149    private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
 150    private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
 151    private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
 152    private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
 153    private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
 154    protected final ArrayList<Message> messages = new ArrayList<>();
 155    public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
 156    protected Account account = null;
 157    private String draftMessage;
 158    private final String name;
 159    private final String contactUuid;
 160    private final String accountUuid;
 161    private Jid contactJid;
 162    private int status;
 163    private final long created;
 164    private int mode;
 165    private JSONObject attributes;
 166    private Jid nextCounterpart;
 167    private transient MucOptions mucOptions = null;
 168    private boolean messagesLeftOnServer = true;
 169    private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
 170    private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
 171    private String mFirstMamReference = null;
 172    protected int mCurrentTab = -1;
 173    protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
 174    protected Element thread = null;
 175    protected boolean lockThread = false;
 176    protected boolean userSelectedThread = false;
 177    protected Message replyTo = null;
 178
 179    public Conversation(final String name, final Account account, final Jid contactJid,
 180                        final int mode) {
 181        this(java.util.UUID.randomUUID().toString(), name, null, account
 182                        .getUuid(), contactJid, System.currentTimeMillis(),
 183                STATUS_AVAILABLE, mode, "");
 184        this.account = account;
 185    }
 186
 187    public Conversation(final String uuid, final String name, final String contactUuid,
 188                        final String accountUuid, final Jid contactJid, final long created, final int status,
 189                        final int mode, final String attributes) {
 190        this.uuid = uuid;
 191        this.name = name;
 192        this.contactUuid = contactUuid;
 193        this.accountUuid = accountUuid;
 194        this.contactJid = contactJid;
 195        this.created = created;
 196        this.status = status;
 197        this.mode = mode;
 198        try {
 199            this.attributes = new JSONObject(attributes == null ? "" : attributes);
 200        } catch (JSONException e) {
 201            this.attributes = new JSONObject();
 202        }
 203    }
 204
 205    public static Conversation fromCursor(Cursor cursor) {
 206        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 207                cursor.getString(cursor.getColumnIndex(NAME)),
 208                cursor.getString(cursor.getColumnIndex(CONTACT)),
 209                cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 210                JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
 211                cursor.getLong(cursor.getColumnIndex(CREATED)),
 212                cursor.getInt(cursor.getColumnIndex(STATUS)),
 213                cursor.getInt(cursor.getColumnIndex(MODE)),
 214                cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 215    }
 216
 217    public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
 218        for (int i = messages.size() - 1; i >= 0; --i) {
 219            final Message message = messages.get(i);
 220            if (message.getStatus() <= Message.STATUS_RECEIVED
 221                    && (message.markable || isPrivateAndNonAnonymousMuc)
 222                    && !message.isPrivateMessage()) {
 223                return message;
 224            }
 225        }
 226        return null;
 227    }
 228
 229    private static boolean suitableForOmemoByDefault(final Conversation conversation) {
 230        if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
 231            return false;
 232        }
 233        if (conversation.getContact().isOwnServer()) {
 234            return false;
 235        }
 236        final String contact = conversation.getJid().getDomain().toEscapedString();
 237        final String account = conversation.getAccount().getServer();
 238        if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
 239            return false;
 240        }
 241        return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
 242    }
 243
 244    public boolean hasMessagesLeftOnServer() {
 245        return messagesLeftOnServer;
 246    }
 247
 248    public void setHasMessagesLeftOnServer(boolean value) {
 249        this.messagesLeftOnServer = value;
 250    }
 251
 252    public Message getFirstUnreadMessage() {
 253        Message first = null;
 254        synchronized (this.messages) {
 255            for (int i = messages.size() - 1; i >= 0; --i) {
 256                if (messages.get(i).isRead()) {
 257                    return first;
 258                } else {
 259                    first = messages.get(i);
 260                }
 261            }
 262        }
 263        return first;
 264    }
 265
 266    public String findMostRecentRemoteDisplayableId() {
 267        final boolean multi = mode == Conversation.MODE_MULTI;
 268        synchronized (this.messages) {
 269            for (final Message message : Lists.reverse(this.messages)) {
 270                if (message.getStatus() == Message.STATUS_RECEIVED) {
 271                    final String serverMsgId = message.getServerMsgId();
 272                    if (serverMsgId != null && multi) {
 273                        return serverMsgId;
 274                    }
 275                    return message.getRemoteMsgId();
 276                }
 277            }
 278        }
 279        return null;
 280    }
 281
 282    public int countFailedDeliveries() {
 283        int count = 0;
 284        synchronized (this.messages) {
 285            for(final Message message : this.messages) {
 286                if (message.getStatus() == Message.STATUS_SEND_FAILED) {
 287                    ++count;
 288                }
 289            }
 290        }
 291        return count;
 292    }
 293
 294    public Message getLastEditableMessage() {
 295        synchronized (this.messages) {
 296            for (final Message message : Lists.reverse(this.messages)) {
 297                if (message.isEditable()) {
 298                    if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
 299                        return null;
 300                    }
 301                    return message;
 302                }
 303            }
 304        }
 305        return null;
 306    }
 307
 308
 309    public Message findUnsentMessageWithUuid(String uuid) {
 310        synchronized (this.messages) {
 311            for (final Message message : this.messages) {
 312                final int s = message.getStatus();
 313                if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 314                    return message;
 315                }
 316            }
 317        }
 318        return null;
 319    }
 320
 321    public void findWaitingMessages(OnMessageFound onMessageFound) {
 322        final ArrayList<Message> results = new ArrayList<>();
 323        synchronized (this.messages) {
 324            for (Message message : this.messages) {
 325                if (message.getStatus() == Message.STATUS_WAITING) {
 326                    results.add(message);
 327                }
 328            }
 329        }
 330        for (Message result : results) {
 331            onMessageFound.onMessageFound(result);
 332        }
 333    }
 334
 335    public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) {
 336        final ArrayList<Message> results = new ArrayList<>();
 337        synchronized (this.messages) {
 338            for (final Message message : this.messages) {
 339                if (message.isRead()) {
 340                    continue;
 341                }
 342                results.add(message);
 343            }
 344        }
 345        for (final Message result : results) {
 346            onMessageFound.onMessageFound(result);
 347        }
 348    }
 349
 350    public Message findMessageWithFileAndUuid(final String uuid) {
 351        synchronized (this.messages) {
 352            for (final Message message : this.messages) {
 353                final Transferable transferable = message.getTransferable();
 354                final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
 355                if (message.getUuid().equals(uuid)
 356                        && message.getEncryption() != Message.ENCRYPTION_PGP
 357                        && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
 358                    return message;
 359                }
 360            }
 361        }
 362        return null;
 363    }
 364
 365    public Message findMessageWithUuid(final String uuid) {
 366        synchronized (this.messages) {
 367            for (final Message message : this.messages) {
 368                if (message.getUuid().equals(uuid)) {
 369                    return message;
 370                }
 371            }
 372        }
 373        return null;
 374    }
 375
 376    public boolean markAsDeleted(final List<String> uuids) {
 377        boolean deleted = false;
 378        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 379        synchronized (this.messages) {
 380            for (Message message : this.messages) {
 381                if (uuids.contains(message.getUuid())) {
 382                    message.setDeleted(true);
 383                    deleted = true;
 384                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 385                        pgpDecryptionService.discard(message);
 386                    }
 387                }
 388            }
 389        }
 390        return deleted;
 391    }
 392
 393    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
 394        boolean changed = false;
 395        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 396        synchronized (this.messages) {
 397            for (Message message : this.messages) {
 398                for (final DatabaseBackend.FilePathInfo file : files)
 399                    if (file.uuid.toString().equals(message.getUuid())) {
 400                        message.setDeleted(file.deleted);
 401                        changed = true;
 402                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 403                            pgpDecryptionService.discard(message);
 404                        }
 405                    }
 406            }
 407        }
 408        return changed;
 409    }
 410
 411    public void clearMessages() {
 412        synchronized (this.messages) {
 413            this.messages.clear();
 414        }
 415    }
 416
 417    public boolean setIncomingChatState(ChatState state) {
 418        if (this.mIncomingChatState == state) {
 419            return false;
 420        }
 421        this.mIncomingChatState = state;
 422        return true;
 423    }
 424
 425    public ChatState getIncomingChatState() {
 426        return this.mIncomingChatState;
 427    }
 428
 429    public boolean setOutgoingChatState(ChatState state) {
 430        if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
 431            if (this.mOutgoingChatState != state) {
 432                this.mOutgoingChatState = state;
 433                return true;
 434            }
 435        }
 436        return false;
 437    }
 438
 439    public ChatState getOutgoingChatState() {
 440        return this.mOutgoingChatState;
 441    }
 442
 443    public void trim() {
 444        synchronized (this.messages) {
 445            final int size = messages.size();
 446            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 447            if (size > maxsize) {
 448                List<Message> discards = this.messages.subList(0, size - maxsize);
 449                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 450                if (pgpDecryptionService != null) {
 451                    pgpDecryptionService.discard(discards);
 452                }
 453                discards.clear();
 454                untieMessages();
 455            }
 456        }
 457    }
 458
 459    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 460        final ArrayList<Message> results = new ArrayList<>();
 461        synchronized (this.messages) {
 462            for (Message message : this.messages) {
 463                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
 464                    results.add(message);
 465                }
 466            }
 467        }
 468        for (Message result : results) {
 469            onMessageFound.onMessageFound(result);
 470        }
 471    }
 472
 473    public Message findSentMessageWithUuidOrRemoteId(String id) {
 474        synchronized (this.messages) {
 475            for (Message message : this.messages) {
 476                if (id.equals(message.getUuid())
 477                        || (message.getStatus() >= Message.STATUS_SEND
 478                        && id.equals(message.getRemoteMsgId()))) {
 479                    return message;
 480                }
 481            }
 482        }
 483        return null;
 484    }
 485
 486    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
 487        synchronized (this.messages) {
 488            for (int i = this.messages.size() - 1; i >= 0; --i) {
 489                final Message message = messages.get(i);
 490                final Jid mcp = message.getCounterpart();
 491                if (mcp == null && counterpart != null) {
 492                    continue;
 493                }
 494                if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
 495                    final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
 496                    if (idMatch) return message;
 497                }
 498            }
 499        }
 500        return null;
 501    }
 502
 503    public Message findSentMessageWithUuid(String id) {
 504        synchronized (this.messages) {
 505            for (Message message : this.messages) {
 506                if (id.equals(message.getUuid())) {
 507                    return message;
 508                }
 509            }
 510        }
 511        return null;
 512    }
 513
 514    public Message findMessageWithRemoteId(String id, Jid counterpart) {
 515        synchronized (this.messages) {
 516            for (Message message : this.messages) {
 517                if (counterpart.equals(message.getCounterpart())
 518                        && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
 519                    return message;
 520                }
 521            }
 522        }
 523        return null;
 524    }
 525
 526    public Message findMessageWithServerMsgId(String id) {
 527        synchronized (this.messages) {
 528            for (Message message : this.messages) {
 529                if (id != null && id.equals(message.getServerMsgId())) {
 530                    return message;
 531                }
 532            }
 533        }
 534        return null;
 535    }
 536
 537    public boolean hasMessageWithCounterpart(Jid counterpart) {
 538        synchronized (this.messages) {
 539            for (Message message : this.messages) {
 540                if (counterpart.equals(message.getCounterpart())) {
 541                    return true;
 542                }
 543            }
 544        }
 545        return false;
 546    }
 547
 548    public Message findMessageReactingTo(String id, Jid reactor) {
 549        if (id == null) return null;
 550
 551        synchronized (this.messages) {
 552            for (int i = this.messages.size() - 1; i >= 0; --i) {
 553                final Message message = messages.get(i);
 554                if (reactor == null && message.getStatus() < Message.STATUS_SEND) continue;
 555                if (reactor != null && !(message.getCounterpart().equals(reactor) || message.getCounterpart().asBareJid().equals(reactor))) continue;
 556
 557                final Element r = message.getReactions();
 558                if (r != null && r.getAttribute("id") != null && id.equals(r.getAttribute("id"))) {
 559                    return message;
 560                }
 561            }
 562        }
 563        return null;
 564    }
 565
 566    public Set<String> findReactionsTo(String id, Jid reactor) {
 567        Set<String> reactionEmoji = new HashSet<>();
 568        Message reactM = findMessageReactingTo(id, reactor);
 569        Element reactions = reactM == null ? null : reactM.getReactions();
 570        if (reactions != null) {
 571            for (Element el : reactions.getChildren()) {
 572                if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
 573                    reactionEmoji.add(el.getContent());
 574                }
 575            }
 576        }
 577        return reactionEmoji;
 578    }
 579
 580    public long loadMoreTimestamp() {
 581        if (messages.size() < 1) return 0;
 582        if (getLockThread() && messages.size() > 5000) return 0;
 583
 584        if (messages.get(0).getType() == Message.TYPE_STATUS && messages.size() >= 2) {
 585            return messages.get(1).getTimeSent();
 586        } else {
 587            return messages.get(0).getTimeSent();
 588        }
 589    }
 590
 591    public void populateWithMessages(final List<Message> messages) {
 592        synchronized (this.messages) {
 593            messages.clear();
 594            messages.addAll(this.messages);
 595        }
 596        Set<String> extraIds = new HashSet<>();
 597        for (ListIterator<Message> iterator = messages.listIterator(messages.size()); iterator.hasPrevious(); ) {
 598            Message m = iterator.previous();
 599            if (m.wasMergedIntoPrevious() || (getLockThread() && !extraIds.contains(m.replyId()) && (m.getThread() == null || !m.getThread().getContent().equals(getThread().getContent())))) {
 600                iterator.remove();
 601            } else if (getLockThread() && m.getThread() != null) {
 602                Element reply = m.getReply();
 603                if (reply != null && reply.getAttribute("id") != null) extraIds.add(reply.getAttribute("id"));
 604                Element reactions = m.getReactions();
 605                if (reactions != null && reactions.getAttribute("id") != null) extraIds.add(reactions.getAttribute("id"));
 606            }
 607        }
 608    }
 609
 610    @Override
 611    public boolean isBlocked() {
 612        return getContact().isBlocked();
 613    }
 614
 615    @Override
 616    public boolean isDomainBlocked() {
 617        return getContact().isDomainBlocked();
 618    }
 619
 620    @Override
 621    public Jid getBlockedJid() {
 622        return getContact().getBlockedJid();
 623    }
 624
 625    public int countMessages() {
 626        synchronized (this.messages) {
 627            return this.messages.size();
 628        }
 629    }
 630
 631    public String getFirstMamReference() {
 632        return this.mFirstMamReference;
 633    }
 634
 635    public void setFirstMamReference(String reference) {
 636        this.mFirstMamReference = reference;
 637    }
 638
 639    public void setLastClearHistory(long time, String reference) {
 640        if (reference != null) {
 641            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
 642        } else {
 643            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
 644        }
 645    }
 646
 647    public MamReference getLastClearHistory() {
 648        return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
 649    }
 650
 651    public List<Jid> getAcceptedCryptoTargets() {
 652        if (mode == MODE_SINGLE) {
 653            return Collections.singletonList(getJid().asBareJid());
 654        } else {
 655            return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 656        }
 657    }
 658
 659    public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 660        setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 661    }
 662
 663    public boolean setCorrectingMessage(Message correctingMessage) {
 664        setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
 665        return correctingMessage == null && draftMessage != null;
 666    }
 667
 668    public Message getCorrectingMessage() {
 669        final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
 670        return uuid == null ? null : findSentMessageWithUuid(uuid);
 671    }
 672
 673    public boolean withSelf() {
 674        return getContact().isSelf();
 675    }
 676
 677    @Override
 678    public int compareTo(@NonNull Conversation another) {
 679        return ComparisonChain.start()
 680                .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
 681                .compare(another.getSortableTime(), getSortableTime())
 682                .result();
 683    }
 684
 685    private long getSortableTime() {
 686        Draft draft = getDraft();
 687        long messageTime = getLatestMessage().getTimeReceived();
 688        if (draft == null) {
 689            return messageTime;
 690        } else {
 691            return Math.max(messageTime, draft.getTimestamp());
 692        }
 693    }
 694
 695    public String getDraftMessage() {
 696        return draftMessage;
 697    }
 698
 699    public void setDraftMessage(String draftMessage) {
 700        this.draftMessage = draftMessage;
 701    }
 702
 703    public Element getThread() {
 704        return this.thread;
 705    }
 706
 707    public void setThread(Element thread) {
 708        this.thread = thread;
 709    }
 710
 711    public void setLockThread(boolean flag) {
 712        this.lockThread = flag;
 713        if (flag) setUserSelectedThread(true);
 714    }
 715
 716    public boolean getLockThread() {
 717        return this.lockThread;
 718    }
 719
 720    public void setUserSelectedThread(boolean flag) {
 721        this.userSelectedThread = flag;
 722    }
 723
 724    public boolean getUserSelectedThread() {
 725        return this.userSelectedThread;
 726    }
 727
 728    public void setReplyTo(Message m) {
 729        this.replyTo = m;
 730    }
 731
 732    public Message getReplyTo() {
 733        return this.replyTo;
 734    }
 735
 736    public boolean isRead() {
 737        synchronized (this.messages) {
 738            for(final Message message : Lists.reverse(this.messages)) {
 739                if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
 740                    continue;
 741                }
 742                return message.isRead();
 743            }
 744            return true;
 745        }
 746    }
 747
 748    public List<Message> markRead(String upToUuid) {
 749        final List<Message> unread = new ArrayList<>();
 750        synchronized (this.messages) {
 751            for (Message message : this.messages) {
 752                if (!message.isRead()) {
 753                    message.markRead();
 754                    unread.add(message);
 755                }
 756                if (message.getUuid().equals(upToUuid)) {
 757                    return unread;
 758                }
 759            }
 760        }
 761        return unread;
 762    }
 763
 764    public Message getLatestMessage() {
 765        synchronized (this.messages) {
 766            if (this.messages.size() == 0) {
 767                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 768                message.setType(Message.TYPE_STATUS);
 769                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 770                message.setTimeReceived(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 771                return message;
 772            } else {
 773                return this.messages.get(this.messages.size() - 1);
 774            }
 775        }
 776    }
 777
 778    public @NonNull
 779    CharSequence getName() {
 780        if (getMode() == MODE_MULTI) {
 781            final String roomName = getMucOptions().getName();
 782            final String subject = getMucOptions().getSubject();
 783            final Bookmark bookmark = getBookmark();
 784            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 785            if (printableValue(roomName)) {
 786                return roomName;
 787            } else if (printableValue(subject)) {
 788                return subject;
 789            } else if (printableValue(bookmarkName, false)) {
 790                return bookmarkName;
 791            } else {
 792                final String generatedName = getMucOptions().createNameFromParticipants();
 793                if (printableValue(generatedName)) {
 794                    return generatedName;
 795                } else {
 796                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 797                }
 798            }
 799        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 800            return contactJid.equals(Jid.of("cheogram.com")) ? "Cheogram" : contactJid;
 801        } else {
 802            return this.getContact().getDisplayName();
 803        }
 804    }
 805
 806    public String getAccountUuid() {
 807        return this.accountUuid;
 808    }
 809
 810    public Account getAccount() {
 811        return this.account;
 812    }
 813
 814    public void setAccount(final Account account) {
 815        this.account = account;
 816    }
 817
 818    public Contact getContact() {
 819        return this.account.getRoster().getContact(this.contactJid);
 820    }
 821
 822    @Override
 823    public Jid getJid() {
 824        return this.contactJid;
 825    }
 826
 827    public int getStatus() {
 828        return this.status;
 829    }
 830
 831    public void setStatus(int status) {
 832        this.status = status;
 833    }
 834
 835    public long getCreated() {
 836        return this.created;
 837    }
 838
 839    public ContentValues getContentValues() {
 840        ContentValues values = new ContentValues();
 841        values.put(UUID, uuid);
 842        values.put(NAME, name);
 843        values.put(CONTACT, contactUuid);
 844        values.put(ACCOUNT, accountUuid);
 845        values.put(CONTACTJID, contactJid.toString());
 846        values.put(CREATED, created);
 847        values.put(STATUS, status);
 848        values.put(MODE, mode);
 849        synchronized (this.attributes) {
 850            values.put(ATTRIBUTES, attributes.toString());
 851        }
 852        return values;
 853    }
 854
 855    public int getMode() {
 856        return this.mode;
 857    }
 858
 859    public void setMode(int mode) {
 860        this.mode = mode;
 861    }
 862
 863    /**
 864     * short for is Private and Non-anonymous
 865     */
 866    public boolean isSingleOrPrivateAndNonAnonymous() {
 867        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 868    }
 869
 870    public boolean isPrivateAndNonAnonymous() {
 871        return getMucOptions().isPrivateAndNonAnonymous();
 872    }
 873
 874    public synchronized MucOptions getMucOptions() {
 875        if (this.mucOptions == null) {
 876            this.mucOptions = new MucOptions(this);
 877        }
 878        return this.mucOptions;
 879    }
 880
 881    public void resetMucOptions() {
 882        this.mucOptions = null;
 883    }
 884
 885    public void setContactJid(final Jid jid) {
 886        this.contactJid = jid;
 887    }
 888
 889    public Jid getNextCounterpart() {
 890        return this.nextCounterpart;
 891    }
 892
 893    public void setNextCounterpart(Jid jid) {
 894        this.nextCounterpart = jid;
 895    }
 896
 897    public int getNextEncryption() {
 898        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 899            return Message.ENCRYPTION_NONE;
 900        }
 901        if (OmemoSetting.isAlways()) {
 902            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 903        }
 904        final int defaultEncryption;
 905        if (suitableForOmemoByDefault(this)) {
 906            defaultEncryption = OmemoSetting.getEncryption();
 907        } else {
 908            defaultEncryption = Message.ENCRYPTION_NONE;
 909        }
 910        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 911        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 912            return defaultEncryption;
 913        } else {
 914            return encryption;
 915        }
 916    }
 917
 918    public boolean setNextEncryption(int encryption) {
 919        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 920    }
 921
 922    public String getNextMessage() {
 923        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 924        return nextMessage == null ? "" : nextMessage;
 925    }
 926
 927    public @Nullable
 928    Draft getDraft() {
 929        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 930        if (timestamp > getLatestMessage().getTimeSent()) {
 931            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 932            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 933                return new Draft(message, timestamp);
 934            }
 935        }
 936        return null;
 937    }
 938
 939    public boolean setNextMessage(final String input) {
 940        final String message = input == null || input.trim().isEmpty() ? null : input;
 941        boolean changed = !getNextMessage().equals(message);
 942        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 943        if (changed) {
 944            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 945        }
 946        return changed;
 947    }
 948
 949    public Bookmark getBookmark() {
 950        return this.account.getBookmark(this.contactJid);
 951    }
 952
 953    public Message findDuplicateMessage(Message message) {
 954        synchronized (this.messages) {
 955            for (int i = this.messages.size() - 1; i >= 0; --i) {
 956                if (this.messages.get(i).similar(message)) {
 957                    return this.messages.get(i);
 958                }
 959            }
 960        }
 961        return null;
 962    }
 963
 964    public boolean hasDuplicateMessage(Message message) {
 965        return findDuplicateMessage(message) != null;
 966    }
 967
 968    public Message findSentMessageWithBody(String body) {
 969        synchronized (this.messages) {
 970            for (int i = this.messages.size() - 1; i >= 0; --i) {
 971                Message message = this.messages.get(i);
 972                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 973                    String otherBody;
 974                    if (message.hasFileOnRemoteHost()) {
 975                        otherBody = message.getFileParams().url;
 976                    } else {
 977                        otherBody = message.body;
 978                    }
 979                    if (otherBody != null && otherBody.equals(body)) {
 980                        return message;
 981                    }
 982                }
 983            }
 984            return null;
 985        }
 986    }
 987
 988    public Message findRtpSession(final String sessionId, final int s) {
 989        synchronized (this.messages) {
 990            for (int i = this.messages.size() - 1; i >= 0; --i) {
 991                final Message message = this.messages.get(i);
 992                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 993                    return message;
 994                }
 995            }
 996        }
 997        return null;
 998    }
 999
1000    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
1001        if (serverMsgId == null || remoteMsgId == null) {
1002            return false;
1003        }
1004        synchronized (this.messages) {
1005            for (Message message : this.messages) {
1006                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
1007                    return true;
1008                }
1009            }
1010        }
1011        return false;
1012    }
1013
1014    public MamReference getLastMessageTransmitted() {
1015        final MamReference lastClear = getLastClearHistory();
1016        MamReference lastReceived = new MamReference(0);
1017        synchronized (this.messages) {
1018            for (int i = this.messages.size() - 1; i >= 0; --i) {
1019                final Message message = this.messages.get(i);
1020                if (message.isPrivateMessage()) {
1021                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
1022                }
1023                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
1024                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
1025                    break;
1026                }
1027            }
1028        }
1029        return MamReference.max(lastClear, lastReceived);
1030    }
1031
1032    public void setMutedTill(long value) {
1033        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
1034    }
1035
1036    public boolean isMuted() {
1037        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
1038    }
1039
1040    public boolean alwaysNotify() {
1041        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
1042    }
1043
1044    public boolean setAttribute(String key, boolean value) {
1045        return setAttribute(key, String.valueOf(value));
1046    }
1047
1048    private boolean setAttribute(String key, long value) {
1049        return setAttribute(key, Long.toString(value));
1050    }
1051
1052    private boolean setAttribute(String key, int value) {
1053        return setAttribute(key, String.valueOf(value));
1054    }
1055
1056    public boolean setAttribute(String key, String value) {
1057        synchronized (this.attributes) {
1058            try {
1059                if (value == null) {
1060                    if (this.attributes.has(key)) {
1061                        this.attributes.remove(key);
1062                        return true;
1063                    } else {
1064                        return false;
1065                    }
1066                } else {
1067                    final String prev = this.attributes.optString(key, null);
1068                    this.attributes.put(key, value);
1069                    return !value.equals(prev);
1070                }
1071            } catch (JSONException e) {
1072                throw new AssertionError(e);
1073            }
1074        }
1075    }
1076
1077    public boolean setAttribute(String key, List<Jid> jids) {
1078        JSONArray array = new JSONArray();
1079        for (Jid jid : jids) {
1080            array.put(jid.asBareJid().toString());
1081        }
1082        synchronized (this.attributes) {
1083            try {
1084                this.attributes.put(key, array);
1085                return true;
1086            } catch (JSONException e) {
1087                return false;
1088            }
1089        }
1090    }
1091
1092    public String getAttribute(String key) {
1093        synchronized (this.attributes) {
1094            return this.attributes.optString(key, null);
1095        }
1096    }
1097
1098    private List<Jid> getJidListAttribute(String key) {
1099        ArrayList<Jid> list = new ArrayList<>();
1100        synchronized (this.attributes) {
1101            try {
1102                JSONArray array = this.attributes.getJSONArray(key);
1103                for (int i = 0; i < array.length(); ++i) {
1104                    try {
1105                        list.add(Jid.of(array.getString(i)));
1106                    } catch (IllegalArgumentException e) {
1107                        //ignored
1108                    }
1109                }
1110            } catch (JSONException e) {
1111                //ignored
1112            }
1113        }
1114        return list;
1115    }
1116
1117    private int getIntAttribute(String key, int defaultValue) {
1118        String value = this.getAttribute(key);
1119        if (value == null) {
1120            return defaultValue;
1121        } else {
1122            try {
1123                return Integer.parseInt(value);
1124            } catch (NumberFormatException e) {
1125                return defaultValue;
1126            }
1127        }
1128    }
1129
1130    public long getLongAttribute(String key, long defaultValue) {
1131        String value = this.getAttribute(key);
1132        if (value == null) {
1133            return defaultValue;
1134        } else {
1135            try {
1136                return Long.parseLong(value);
1137            } catch (NumberFormatException e) {
1138                return defaultValue;
1139            }
1140        }
1141    }
1142
1143    public boolean getBooleanAttribute(String key, boolean defaultValue) {
1144        String value = this.getAttribute(key);
1145        if (value == null) {
1146            return defaultValue;
1147        } else {
1148            return Boolean.parseBoolean(value);
1149        }
1150    }
1151
1152    public void add(Message message) {
1153        synchronized (this.messages) {
1154            this.messages.add(message);
1155        }
1156    }
1157
1158    public void prepend(int offset, Message message) {
1159        synchronized (this.messages) {
1160            this.messages.add(Math.min(offset, this.messages.size()), message);
1161        }
1162    }
1163
1164    public void addAll(int index, List<Message> messages) {
1165        synchronized (this.messages) {
1166            this.messages.addAll(index, messages);
1167        }
1168        account.getPgpDecryptionService().decrypt(messages);
1169    }
1170
1171    public void expireOldMessages(long timestamp) {
1172        synchronized (this.messages) {
1173            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
1174                if (iterator.next().getTimeSent() < timestamp) {
1175                    iterator.remove();
1176                }
1177            }
1178            untieMessages();
1179        }
1180    }
1181
1182    public void sort() {
1183        synchronized (this.messages) {
1184            Collections.sort(this.messages, (left, right) -> {
1185                if (left.getTimeSent() < right.getTimeSent()) {
1186                    return -1;
1187                } else if (left.getTimeSent() > right.getTimeSent()) {
1188                    return 1;
1189                } else {
1190                    return 0;
1191                }
1192            });
1193            untieMessages();
1194        }
1195    }
1196
1197    private void untieMessages() {
1198        for (Message message : this.messages) {
1199            message.untie();
1200        }
1201    }
1202
1203    public int unreadCount() {
1204        synchronized (this.messages) {
1205            int count = 0;
1206            for(final Message message : Lists.reverse(this.messages)) {
1207                if (message.isRead()) {
1208                    if (message.getType() == Message.TYPE_RTP_SESSION) {
1209                        continue;
1210                    }
1211                    return count;
1212                }
1213                ++count;
1214            }
1215            return count;
1216        }
1217    }
1218
1219    public int receivedMessagesCount() {
1220        int count = 0;
1221        synchronized (this.messages) {
1222            for (Message message : messages) {
1223                if (message.getStatus() == Message.STATUS_RECEIVED) {
1224                    ++count;
1225                }
1226            }
1227        }
1228        return count;
1229    }
1230
1231    public int sentMessagesCount() {
1232        int count = 0;
1233        synchronized (this.messages) {
1234            for (Message message : messages) {
1235                if (message.getStatus() != Message.STATUS_RECEIVED) {
1236                    ++count;
1237                }
1238            }
1239        }
1240        return count;
1241    }
1242
1243    public boolean canInferPresence() {
1244        final Contact contact = getContact();
1245        if (contact != null && contact.canInferPresence()) return true;
1246        return sentMessagesCount() > 0;
1247    }
1248
1249    public boolean isWithStranger() {
1250        final Contact contact = getContact();
1251        return mode == MODE_SINGLE
1252                && !contact.isOwnServer()
1253                && !contact.showInContactList()
1254                && !contact.isSelf()
1255                && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
1256                && sentMessagesCount() == 0;
1257    }
1258
1259    public int getReceivedMessagesCountSinceUuid(String uuid) {
1260        if (uuid == null) {
1261            return 0;
1262        }
1263        int count = 0;
1264        synchronized (this.messages) {
1265            for (int i = messages.size() - 1; i >= 0; i--) {
1266                final Message message = messages.get(i);
1267                if (uuid.equals(message.getUuid())) {
1268                    return count;
1269                }
1270                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1271                    ++count;
1272                }
1273            }
1274        }
1275        return 0;
1276    }
1277
1278    @Override
1279    public int getAvatarBackgroundColor() {
1280        return UIHelper.getColorForName(getName().toString());
1281    }
1282
1283    @Override
1284    public String getAvatarName() {
1285        return getName().toString();
1286    }
1287
1288    public void setCurrentTab(int tab) {
1289        mCurrentTab = tab;
1290    }
1291
1292    public int getCurrentTab() {
1293        if (mCurrentTab >= 0) return mCurrentTab;
1294
1295        if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
1296            return 0;
1297        }
1298
1299        return 1;
1300    }
1301
1302    public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1303        pagerAdapter.startCommand(command, xmppConnectionService);
1304    }
1305
1306    public void setupViewPager(ViewPager pager, TabLayout tabs) {
1307        pagerAdapter.setupViewPager(pager, tabs);
1308    }
1309
1310    public void showViewPager() {
1311        pagerAdapter.show();
1312    }
1313
1314    public void hideViewPager() {
1315        pagerAdapter.hide();
1316    }
1317
1318    public interface OnMessageFound {
1319        void onMessageFound(final Message message);
1320    }
1321
1322    public static class Draft {
1323        private final String message;
1324        private final long timestamp;
1325
1326        private Draft(String message, long timestamp) {
1327            this.message = message;
1328            this.timestamp = timestamp;
1329        }
1330
1331        public long getTimestamp() {
1332            return timestamp;
1333        }
1334
1335        public String getMessage() {
1336            return message;
1337        }
1338    }
1339
1340    public class ConversationPagerAdapter extends PagerAdapter {
1341        protected ViewPager mPager = null;
1342        protected TabLayout mTabs = null;
1343        ArrayList<CommandSession> sessions = null;
1344        protected View page1 = null;
1345        protected View page2 = null;
1346
1347        public void setupViewPager(ViewPager pager, TabLayout tabs) {
1348            mPager = pager;
1349            mTabs = tabs;
1350
1351            if (mPager == null) return;
1352            if (sessions != null) show();
1353
1354            page1 = pager.getChildAt(0) == null ? page1 : pager.getChildAt(0);
1355            page2 = pager.getChildAt(1) == null ? page2 : pager.getChildAt(1);
1356            pager.setAdapter(this);
1357            tabs.setupWithViewPager(mPager);
1358            pager.post(() -> pager.setCurrentItem(getCurrentTab()));
1359
1360            mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1361                public void onPageScrollStateChanged(int state) { }
1362                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1363
1364                public void onPageSelected(int position) {
1365                    setCurrentTab(position);
1366                }
1367            });
1368        }
1369
1370        public void show() {
1371            if (sessions == null) {
1372                sessions = new ArrayList<>();
1373                notifyDataSetChanged();
1374            }
1375            if (mTabs != null) mTabs.setVisibility(View.VISIBLE);
1376        }
1377
1378        public void hide() {
1379            if (sessions != null && !sessions.isEmpty()) return; // Do not hide during active session
1380            if (mPager != null) mPager.setCurrentItem(0);
1381            if (mTabs != null) mTabs.setVisibility(View.GONE);
1382            sessions = null;
1383            notifyDataSetChanged();
1384        }
1385
1386        public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1387            show();
1388            CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
1389
1390            final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1391            packet.setTo(command.getAttributeAsJid("jid"));
1392            final Element c = packet.addChild("command", Namespace.COMMANDS);
1393            c.setAttribute("node", command.getAttribute("node"));
1394            c.setAttribute("action", "execute");
1395
1396            if (command.getAttribute("node").equals("jabber:iq:register") && packet.getTo().asBareJid().equals(Jid.of("cheogram.com"))) {
1397                new com.cheogram.android.CheogramLicenseChecker(mPager.getContext(), (signedData, signature) -> {
1398                    if (signedData != null && signature != null) {
1399                        c.addChild("license", "https://ns.cheogram.com/google-play").setContent(signedData);
1400                        c.addChild("licenseSignature", "https://ns.cheogram.com/google-play").setContent(signature);
1401                    }
1402
1403                    xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1404                        session.updateWithResponse(iq);
1405                    });
1406                }).checkLicense();
1407            } else {
1408                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1409                    session.updateWithResponse(iq);
1410                });
1411            }
1412
1413            sessions.add(session);
1414            notifyDataSetChanged();
1415            if (mPager != null) mPager.setCurrentItem(getCount() - 1);
1416        }
1417
1418        public void removeSession(CommandSession session) {
1419            sessions.remove(session);
1420            notifyDataSetChanged();
1421        }
1422
1423        @NonNull
1424        @Override
1425        public Object instantiateItem(@NonNull ViewGroup container, int position) {
1426            if (position == 0) {
1427                if (page1.getParent() == null) container.addView(page1);
1428                return page1;
1429            }
1430            if (position == 1) {
1431                if (page2.getParent() == null) container.addView(page2);
1432                return page2;
1433            }
1434
1435            CommandSession session = sessions.get(position-2);
1436            CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
1437            container.addView(binding.getRoot());
1438            session.setBinding(binding);
1439            return session;
1440        }
1441
1442        @Override
1443        public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1444            if (position < 2) return;
1445
1446            container.removeView(((CommandSession) o).getView());
1447        }
1448
1449        @Override
1450        public int getItemPosition(Object o) {
1451            if (mPager != null) {
1452                if (o == page1) return PagerAdapter.POSITION_UNCHANGED;
1453                if (o == page2) return PagerAdapter.POSITION_UNCHANGED;
1454            }
1455
1456            int pos = sessions == null ? -1 : sessions.indexOf(o);
1457            if (pos < 0) return PagerAdapter.POSITION_NONE;
1458            return pos + 2;
1459        }
1460
1461        @Override
1462        public int getCount() {
1463            if (sessions == null) return 1;
1464
1465            int count = 2 + sessions.size();
1466            if (mTabs == null) return count;
1467
1468            if (count > 2) {
1469                mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1470            } else {
1471                mTabs.setTabMode(TabLayout.MODE_FIXED);
1472            }
1473            return count;
1474        }
1475
1476        @Override
1477        public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1478            if (view == o) return true;
1479
1480            if (o instanceof CommandSession) {
1481                return ((CommandSession) o).getView() == view;
1482            }
1483
1484            return false;
1485        }
1486
1487        @Nullable
1488        @Override
1489        public CharSequence getPageTitle(int position) {
1490            switch (position) {
1491                case 0:
1492                    return "Conversation";
1493                case 1:
1494                    return "Commands";
1495                default:
1496                    CommandSession session = sessions.get(position-2);
1497                    if (session == null) return super.getPageTitle(position);
1498                    return session.getTitle();
1499            }
1500        }
1501
1502        class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
1503            abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1504                protected T binding;
1505
1506                public ViewHolder(T binding) {
1507                    super(binding.getRoot());
1508                    this.binding = binding;
1509                }
1510
1511                abstract public void bind(Item el);
1512
1513                protected void setTextOrHide(TextView v, Optional<String> s) {
1514                    if (s == null || !s.isPresent()) {
1515                        v.setVisibility(View.GONE);
1516                    } else {
1517                        v.setVisibility(View.VISIBLE);
1518                        v.setText(s.get());
1519                    }
1520                }
1521
1522                protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
1523                    int flags = 0;
1524                    if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
1525                    textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1526
1527                    String type = field.getAttribute("type");
1528                    if (type != null) {
1529                        if (type.equals("text-multi") || type.equals("jid-multi")) {
1530                            flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
1531                        }
1532
1533                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1534
1535                        if (type.equals("jid-single") || type.equals("jid-multi")) {
1536                            textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1537                        }
1538
1539                        if (type.equals("text-private")) {
1540                            textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1541                            if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
1542                        }
1543                    }
1544
1545                    Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1546                    if (validate == null) return;
1547                    String datatype = validate.getAttribute("datatype");
1548                    if (datatype == null) return;
1549
1550                    if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
1551                        textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
1552                    }
1553
1554                    if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
1555                        textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
1556                    }
1557
1558                    if (datatype.equals("xs:date")) {
1559                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
1560                    }
1561
1562                    if (datatype.equals("xs:dateTime")) {
1563                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
1564                    }
1565
1566                    if (datatype.equals("xs:time")) {
1567                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
1568                    }
1569
1570                    if (datatype.equals("xs:anyURI")) {
1571                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
1572                    }
1573
1574                    if (datatype.equals("html:tel")) {
1575                        textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
1576                    }
1577
1578                    if (datatype.equals("html:email")) {
1579                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1580                    }
1581                }
1582            }
1583
1584            class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1585                public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1586
1587                @Override
1588                public void bind(Item iq) {
1589                    binding.errorIcon.setVisibility(View.VISIBLE);
1590
1591                    Element error = iq.el.findChild("error");
1592                    if (error == null) return;
1593                    String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1594                    if (text == null || text.equals("")) {
1595                        text = error.getChildren().get(0).getName();
1596                    }
1597                    binding.message.setText(text);
1598                }
1599            }
1600
1601            class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1602                public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1603
1604                @Override
1605                public void bind(Item note) {
1606                    binding.message.setText(note.el.getContent());
1607
1608                    String type = note.el.getAttribute("type");
1609                    if (type != null && type.equals("error")) {
1610                        binding.errorIcon.setVisibility(View.VISIBLE);
1611                    }
1612                }
1613            }
1614
1615            class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1616                public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1617
1618                @Override
1619                public void bind(Item item) {
1620                    Field field = (Field) item;
1621                    setTextOrHide(binding.label, field.getLabel());
1622                    setTextOrHide(binding.desc, field.getDesc());
1623
1624                    ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
1625                    for (Element el : field.el.getChildren()) {
1626                        if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1627                            values.add(el.getContent());
1628                        }
1629                    }
1630                    binding.values.setAdapter(values);
1631
1632                    if (field.getType().equals(Optional.of("jid-single")) || field.getType().equals(Optional.of("jid-multi"))) {
1633                        binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
1634                            new FixedURLSpan("xmpp:" + Jid.ofEscaped(values.getItem(pos)).toEscapedString()).onClick(binding.values);
1635                        });
1636                    }
1637
1638                    binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
1639                        if (ShareUtil.copyTextToClipboard(binding.getRoot().getContext(), values.getItem(pos), R.string.message)) {
1640                            Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1641                        }
1642                        return true;
1643                    });
1644                }
1645            }
1646
1647            class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
1648                public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
1649
1650                @Override
1651                public void bind(Item item) {
1652                    Cell cell = (Cell) item;
1653
1654                    if (cell.el == null) {
1655                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
1656                        setTextOrHide(binding.text, cell.reported.getLabel());
1657                    } else {
1658                        String value = cell.el.findChildContent("value", "jabber:x:data");
1659                        SpannableStringBuilder text = new SpannableStringBuilder(value == null ? "" : value);
1660                        if (cell.reported.getType().equals(Optional.of("jid-single"))) {
1661                            text.setSpan(new FixedURLSpan("xmpp:" + Jid.ofEscaped(text.toString()).toEscapedString()), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1662                        }
1663
1664                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
1665                        binding.text.setText(text);
1666
1667                        BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance();
1668                        method.setOnLinkLongClickListener((tv, url) -> {
1669                            tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
1670                            ShareUtil.copyLinkToClipboard(binding.getRoot().getContext(), url);
1671                            return true;
1672                        });
1673                        binding.text.setMovementMethod(method);
1674                    }
1675                }
1676            }
1677
1678            class ItemCardViewHolder extends ViewHolder<CommandItemCardBinding> {
1679                public ItemCardViewHolder(CommandItemCardBinding binding) { super(binding); }
1680
1681                @Override
1682                public void bind(Item item) {
1683                    binding.fields.removeAllViews();
1684
1685                    for (Field field : reported) {
1686                        CommandResultFieldBinding row = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.command_result_field, binding.fields, false);
1687                        GridLayout.LayoutParams param = new GridLayout.LayoutParams();
1688                        param.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f);
1689                        param.width = 0;
1690                        row.getRoot().setLayoutParams(param);
1691                        binding.fields.addView(row.getRoot());
1692                        for (Element el : item.el.getChildren()) {
1693                            if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data") && el.getAttribute("var") != null && el.getAttribute("var").equals(field.getVar())) {
1694                                for (String label : field.getLabel().asSet()) {
1695                                    el.setAttribute("label", label);
1696                                }
1697                                for (String desc : field.getDesc().asSet()) {
1698                                    el.setAttribute("desc", desc);
1699                                }
1700                                for (String type : field.getType().asSet()) {
1701                                    el.setAttribute("type", type);
1702                                }
1703                                Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1704                                if (validate != null) el.addChild(validate);
1705                                new ResultFieldViewHolder(row).bind(new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), -1));
1706                            }
1707                        }
1708                    }
1709                }
1710            }
1711
1712            class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
1713                public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
1714                    super(binding);
1715                    binding.row.setOnClickListener((v) -> {
1716                        binding.checkbox.toggle();
1717                    });
1718                    binding.checkbox.setOnCheckedChangeListener(this);
1719                }
1720                protected Element mValue = null;
1721
1722                @Override
1723                public void bind(Item item) {
1724                    Field field = (Field) item;
1725                    binding.label.setText(field.getLabel().or(""));
1726                    setTextOrHide(binding.desc, field.getDesc());
1727                    mValue = field.getValue();
1728                    binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
1729                }
1730
1731                @Override
1732                public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
1733                    if (mValue == null) return;
1734
1735                    mValue.setContent(isChecked ? "true" : "false");
1736                }
1737            }
1738
1739            class SearchListFieldViewHolder extends ViewHolder<CommandSearchListFieldBinding> implements TextWatcher {
1740                public SearchListFieldViewHolder(CommandSearchListFieldBinding binding) {
1741                    super(binding);
1742                    binding.search.addTextChangedListener(this);
1743                }
1744                protected Element mValue = null;
1745                List<Option> options = new ArrayList<>();
1746                protected ArrayAdapter<Option> adapter;
1747                protected boolean open;
1748
1749                @Override
1750                public void bind(Item item) {
1751                    Field field = (Field) item;
1752                    setTextOrHide(binding.label, field.getLabel());
1753                    setTextOrHide(binding.desc, field.getDesc());
1754
1755                    if (field.error != null) {
1756                        binding.desc.setVisibility(View.VISIBLE);
1757                        binding.desc.setText(field.error);
1758                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1759                    } else {
1760                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1761                    }
1762
1763                    mValue = field.getValue();
1764
1765                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1766                    open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
1767                    setupInputType(field.el, binding.search, null);
1768
1769                    options = field.getOptions();
1770                    binding.list.setOnItemClickListener((parent, view, position, id) -> {
1771                        mValue.setContent(adapter.getItem(binding.list.getCheckedItemPosition()).getValue());
1772                        if (open) binding.search.setText(mValue.getContent());
1773                    });
1774                    search("");
1775                }
1776
1777                @Override
1778                public void afterTextChanged(Editable s) {
1779                    if (open) mValue.setContent(s.toString());
1780                    search(s.toString());
1781                }
1782
1783                @Override
1784                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1785
1786                @Override
1787                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1788
1789                protected void search(String s) {
1790                    List<Option> filteredOptions;
1791                    final String q = s.replaceAll("\\W", "").toLowerCase();
1792                    if (q == null || q.equals("")) {
1793                        filteredOptions = options;
1794                    } else {
1795                        filteredOptions = options.stream().filter(o -> o.toString().replaceAll("\\W", "").toLowerCase().contains(q)).collect(Collectors.toList());
1796                    }
1797                    adapter = new ArrayAdapter(binding.getRoot().getContext(), R.layout.simple_list_item, filteredOptions);
1798                    binding.list.setAdapter(adapter);
1799
1800                    int checkedPos = filteredOptions.indexOf(new Option(mValue.getContent(), ""));
1801                    if (checkedPos >= 0) binding.list.setItemChecked(checkedPos, true);
1802                }
1803            }
1804
1805            class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
1806                public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
1807                    super(binding);
1808                    binding.open.addTextChangedListener(this);
1809                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
1810                        @Override
1811                        public View getView(int position, View convertView, ViewGroup parent) {
1812                            CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
1813                            v.setId(position);
1814                            v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
1815                            v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
1816                            return v;
1817                        }
1818                    };
1819                }
1820                protected Element mValue = null;
1821                protected ArrayAdapter<Option> options;
1822
1823                @Override
1824                public void bind(Item item) {
1825                    Field field = (Field) item;
1826                    setTextOrHide(binding.label, field.getLabel());
1827                    setTextOrHide(binding.desc, field.getDesc());
1828
1829                    if (field.error != null) {
1830                        binding.desc.setVisibility(View.VISIBLE);
1831                        binding.desc.setText(field.error);
1832                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1833                    } else {
1834                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1835                    }
1836
1837                    mValue = field.getValue();
1838
1839                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1840                    binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
1841                    binding.open.setText(mValue.getContent());
1842                    setupInputType(field.el, binding.open, null);
1843
1844                    options.clear();
1845                    List<Option> theOptions = field.getOptions();
1846                    options.addAll(theOptions);
1847
1848                    float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
1849                    TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
1850                    float maxColumnWidth = theOptions.stream().map((x) ->
1851                        StaticLayout.getDesiredWidth(x.toString(), paint)
1852                    ).max(Float::compare).orElse(new Float(0.0));
1853                    if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
1854                        binding.radios.setNumColumns(theOptions.size());
1855                    } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
1856                        binding.radios.setNumColumns(theOptions.size() / 2);
1857                    } else {
1858                        binding.radios.setNumColumns(1);
1859                    }
1860                    binding.radios.setAdapter(options);
1861                }
1862
1863                @Override
1864                public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
1865                    if (mValue == null) return;
1866
1867                    if (isChecked) {
1868                        mValue.setContent(options.getItem(radio.getId()).getValue());
1869                        binding.open.setText(mValue.getContent());
1870                    }
1871                    options.notifyDataSetChanged();
1872                }
1873
1874                @Override
1875                public void afterTextChanged(Editable s) {
1876                    if (mValue == null) return;
1877
1878                    mValue.setContent(s.toString());
1879                    options.notifyDataSetChanged();
1880                }
1881
1882                @Override
1883                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1884
1885                @Override
1886                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1887            }
1888
1889            class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
1890                public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
1891                    super(binding);
1892                    binding.spinner.setOnItemSelectedListener(this);
1893                }
1894                protected Element mValue = null;
1895
1896                @Override
1897                public void bind(Item item) {
1898                    Field field = (Field) item;
1899                    setTextOrHide(binding.label, field.getLabel());
1900                    binding.spinner.setPrompt(field.getLabel().or(""));
1901                    setTextOrHide(binding.desc, field.getDesc());
1902
1903                    mValue = field.getValue();
1904
1905                    ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
1906                    options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1907                    options.addAll(field.getOptions());
1908
1909                    binding.spinner.setAdapter(options);
1910                    binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
1911                }
1912
1913                @Override
1914                public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
1915                    Option o = (Option) parent.getItemAtPosition(pos);
1916                    if (mValue == null) return;
1917
1918                    mValue.setContent(o == null ? "" : o.getValue());
1919                }
1920
1921                @Override
1922                public void onNothingSelected(AdapterView<?> parent) {
1923                    mValue.setContent("");
1924                }
1925            }
1926
1927            class ButtonGridFieldViewHolder extends ViewHolder<CommandButtonGridFieldBinding> {
1928                public ButtonGridFieldViewHolder(CommandButtonGridFieldBinding binding) {
1929                    super(binding);
1930                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.button_grid_item) {
1931                        @Override
1932                        public View getView(int position, View convertView, ViewGroup parent) {
1933                            Button v = (Button) super.getView(position, convertView, parent);
1934                            v.setOnClickListener((view) -> {
1935                                loading = true;
1936                                mValue.setContent(getItem(position).getValue());
1937                                execute();
1938                            });
1939
1940                            final SVG icon = getItem(position).getIcon();
1941                            if (icon != null) {
1942                                 v.post(() -> {
1943                                     if (v.getHeight() == 0) return;
1944                                     icon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
1945                                     Bitmap bitmap = Bitmap.createBitmap(v.getHeight(), v.getHeight(), Bitmap.Config.ARGB_8888);
1946                                     Canvas bmcanvas = new Canvas(bitmap);
1947                                     icon.renderToCanvas(bmcanvas);
1948                                     v.setCompoundDrawablesRelativeWithIntrinsicBounds(new BitmapDrawable(bitmap), null, null, null);
1949                                 });
1950                            }
1951
1952                            return v;
1953                        }
1954                    };
1955                }
1956                protected Element mValue = null;
1957                protected ArrayAdapter<Option> options;
1958                protected Option defaultOption = null;
1959
1960                @Override
1961                public void bind(Item item) {
1962                    Field field = (Field) item;
1963                    setTextOrHide(binding.label, field.getLabel());
1964                    setTextOrHide(binding.desc, field.getDesc());
1965
1966                    if (field.error != null) {
1967                        binding.desc.setVisibility(View.VISIBLE);
1968                        binding.desc.setText(field.error);
1969                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1970                    } else {
1971                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1972                    }
1973
1974                    mValue = field.getValue();
1975
1976                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1977                    binding.openButton.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
1978                    binding.openButton.setOnClickListener((view) -> {
1979                        AlertDialog.Builder builder = new AlertDialog.Builder(binding.getRoot().getContext());
1980                        DialogQuickeditBinding dialogBinding = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.dialog_quickedit, null, false);
1981                        builder.setPositiveButton(R.string.action_execute, null);
1982                        if (field.getDesc().isPresent()) {
1983                            dialogBinding.inputLayout.setHint(field.getDesc().get());
1984                        }
1985                        dialogBinding.inputEditText.requestFocus();
1986                        dialogBinding.inputEditText.getText().append(mValue.getContent());
1987                        builder.setView(dialogBinding.getRoot());
1988                        builder.setNegativeButton(R.string.cancel, null);
1989                        final AlertDialog dialog = builder.create();
1990                        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(dialogBinding.inputEditText));
1991                        dialog.show();
1992                        View.OnClickListener clickListener = v -> {
1993                            loading = true;
1994                            String value = dialogBinding.inputEditText.getText().toString();
1995                            mValue.setContent(value);
1996                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
1997                            dialog.dismiss();
1998                            execute();
1999                        };
2000                        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
2001                        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
2002                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2003                            dialog.dismiss();
2004                        }));
2005                        dialog.setCanceledOnTouchOutside(false);
2006                        dialog.setOnDismissListener(dialog1 -> {
2007                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2008                        });
2009                    });
2010
2011                    options.clear();
2012                    List<Option> theOptions = field.getType().equals(Optional.of("boolean")) ? new ArrayList<>(List.of(new Option("false", binding.getRoot().getContext().getString(R.string.no)), new Option("true", binding.getRoot().getContext().getString(R.string.yes)))) : field.getOptions();
2013
2014                    defaultOption = null;
2015                    for (Option option : theOptions) {
2016                        if (option.getValue().equals(mValue.getContent())) {
2017                            defaultOption = option;
2018                            break;
2019                        }
2020                    }
2021                    if (defaultOption == null && !mValue.getContent().equals("")) {
2022                        // Synthesize default option for custom value
2023                        defaultOption = new Option(mValue.getContent(), mValue.getContent());
2024                    }
2025                    if (defaultOption == null) {
2026                        binding.defaultButton.setVisibility(View.GONE);
2027                    } else {
2028                        theOptions.remove(defaultOption);
2029                        binding.defaultButton.setVisibility(View.VISIBLE);
2030
2031                        final SVG defaultIcon = defaultOption.getIcon();
2032                        if (defaultIcon != null) {
2033                             defaultIcon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
2034                             DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
2035                             Bitmap bitmap = Bitmap.createBitmap((int)(display.heightPixels*display.density/4), (int)(display.heightPixels*display.density/4), Bitmap.Config.ARGB_8888);
2036                             bitmap.setDensity(display.densityDpi);
2037                             Canvas bmcanvas = new Canvas(bitmap);
2038                             defaultIcon.renderToCanvas(bmcanvas);
2039                             binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
2040                        }
2041
2042                        binding.defaultButton.setText(defaultOption.toString());
2043                        binding.defaultButton.setOnClickListener((view) -> {
2044                            loading = true;
2045                            mValue.setContent(defaultOption.getValue());
2046                            execute();
2047                        });
2048                    }
2049
2050                    options.addAll(theOptions);
2051                    binding.buttons.setAdapter(options);
2052                }
2053            }
2054
2055            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
2056                public TextFieldViewHolder(CommandTextFieldBinding binding) {
2057                    super(binding);
2058                    binding.textinput.addTextChangedListener(this);
2059                }
2060                protected Element mValue = null;
2061
2062                @Override
2063                public void bind(Item item) {
2064                    Field field = (Field) item;
2065                    binding.textinputLayout.setHint(field.getLabel().or(""));
2066
2067                    binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
2068                    for (String desc : field.getDesc().asSet()) {
2069                        binding.textinputLayout.setHelperText(desc);
2070                    }
2071
2072                    binding.textinputLayout.setErrorEnabled(field.error != null);
2073                    if (field.error != null) binding.textinputLayout.setError(field.error);
2074
2075                    binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
2076                    String suffixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/suffix-label");
2077                    if (suffixLabel != null) {
2078                        binding.textinputLayout.setSuffixText(suffixLabel);
2079                        binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END);
2080                    }
2081
2082                    String prefixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/prefix-label");
2083                    if (prefixLabel != null) {
2084                        binding.textinputLayout.setPrefixText(prefixLabel);
2085                    }
2086
2087                    mValue = field.getValue();
2088                    binding.textinput.setText(mValue.getContent());
2089                    setupInputType(field.el, binding.textinput, binding.textinputLayout);
2090                }
2091
2092                @Override
2093                public void afterTextChanged(Editable s) {
2094                    if (mValue == null) return;
2095
2096                    mValue.setContent(s.toString());
2097                }
2098
2099                @Override
2100                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
2101
2102                @Override
2103                public void onTextChanged(CharSequence s, int start, int count, int after) { }
2104            }
2105
2106            class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
2107                public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
2108                protected String boundUrl = "";
2109
2110                @Override
2111                public void bind(Item oob) {
2112                    setTextOrHide(binding.desc, Optional.fromNullable(oob.el.findChildContent("desc", "jabber:x:oob")));
2113                    binding.webview.getSettings().setJavaScriptEnabled(true);
2114                    binding.webview.getSettings().setUserAgentString("Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36");
2115                    binding.webview.getSettings().setDatabaseEnabled(true);
2116                    binding.webview.getSettings().setDomStorageEnabled(true);
2117                    binding.webview.setWebChromeClient(new WebChromeClient() {
2118                        @Override
2119                        public void onProgressChanged(WebView view, int newProgress) {
2120                            binding.progressbar.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
2121                            binding.progressbar.setProgress(newProgress);
2122                        }
2123                    });
2124                    binding.webview.setWebViewClient(new WebViewClient() {
2125                        @Override
2126                        public void onPageFinished(WebView view, String url) {
2127                            super.onPageFinished(view, url);
2128                            mTitle = view.getTitle();
2129                            ConversationPagerAdapter.this.notifyDataSetChanged();
2130                        }
2131                    });
2132                    final String url = oob.el.findChildContent("url", "jabber:x:oob");
2133                    if (!boundUrl.equals(url)) {
2134                        binding.webview.addJavascriptInterface(new JsObject(), "xmpp_xep0050");
2135                        binding.webview.loadUrl(url);
2136                        boundUrl = url;
2137                    }
2138                }
2139
2140                class JsObject {
2141                    @JavascriptInterface
2142                    public void execute() { execute("execute"); }
2143
2144                    @JavascriptInterface
2145                    public void execute(String action) {
2146                        getView().post(() -> {
2147                            actionToWebview = null;
2148                            if(CommandSession.this.execute(action)) {
2149                                removeSession(CommandSession.this);
2150                            }
2151                        });
2152                    }
2153
2154                    @JavascriptInterface
2155                    public void preventDefault() {
2156                        actionToWebview = binding.webview;
2157                    }
2158                }
2159            }
2160
2161            class ProgressBarViewHolder extends ViewHolder<CommandProgressBarBinding> {
2162                public ProgressBarViewHolder(CommandProgressBarBinding binding) { super(binding); }
2163
2164                @Override
2165                public void bind(Item item) { }
2166            }
2167
2168            class Item {
2169                protected Element el;
2170                protected int viewType;
2171                protected String error = null;
2172
2173                Item(Element el, int viewType) {
2174                    this.el = el;
2175                    this.viewType = viewType;
2176                }
2177
2178                public boolean validate() {
2179                    error = null;
2180                    return true;
2181                }
2182            }
2183
2184            class Field extends Item {
2185                Field(eu.siacs.conversations.xmpp.forms.Field el, int viewType) { super(el, viewType); }
2186
2187                @Override
2188                public boolean validate() {
2189                    if (!super.validate()) return false;
2190                    if (el.findChild("required", "jabber:x:data") == null) return true;
2191                    if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
2192
2193                    error = "this value is required";
2194                    return false;
2195                }
2196
2197                public String getVar() {
2198                    return el.getAttribute("var");
2199                }
2200
2201                public Optional<String> getType() {
2202                    return Optional.fromNullable(el.getAttribute("type"));
2203                }
2204
2205                public Optional<String> getLabel() {
2206                    String label = el.getAttribute("label");
2207                    if (label == null) label = getVar();
2208                    return Optional.fromNullable(label);
2209                }
2210
2211                public Optional<String> getDesc() {
2212                    return Optional.fromNullable(el.findChildContent("desc", "jabber:x:data"));
2213                }
2214
2215                public Element getValue() {
2216                    Element value = el.findChild("value", "jabber:x:data");
2217                    if (value == null) {
2218                        value = el.addChild("value", "jabber:x:data");
2219                    }
2220                    return value;
2221                }
2222
2223                public List<Option> getOptions() {
2224                    return Option.forField(el);
2225                }
2226            }
2227
2228            class Cell extends Item {
2229                protected Field reported;
2230
2231                Cell(Field reported, Element item) {
2232                    super(item, TYPE_RESULT_CELL);
2233                    this.reported = reported;
2234                }
2235            }
2236
2237            protected Field mkField(Element el) {
2238                int viewType = -1;
2239
2240                String formType = responseElement.getAttribute("type");
2241                if (formType != null) {
2242                    String fieldType = el.getAttribute("type");
2243                    if (fieldType == null) fieldType = "text-single";
2244
2245                    if (formType.equals("result") || fieldType.equals("fixed")) {
2246                        viewType = TYPE_RESULT_FIELD;
2247                    } else if (formType.equals("form")) {
2248                        if (fieldType.equals("boolean")) {
2249                            if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2250                                viewType = TYPE_BUTTON_GRID_FIELD;
2251                            } else {
2252                                viewType = TYPE_CHECKBOX_FIELD;
2253                            }
2254                        } else if (fieldType.equals("list-single")) {
2255                            Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2256                            if (Option.forField(el).size() > 9) {
2257                                viewType = TYPE_SEARCH_LIST_FIELD;
2258                            } else if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2259                                viewType = TYPE_BUTTON_GRID_FIELD;
2260                            } else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
2261                                viewType = TYPE_RADIO_EDIT_FIELD;
2262                            } else {
2263                                viewType = TYPE_SPINNER_FIELD;
2264                            }
2265                        } else {
2266                            viewType = TYPE_TEXT_FIELD;
2267                        }
2268                    }
2269
2270                    return new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), viewType);
2271                }
2272
2273                return null;
2274            }
2275
2276            protected Item mkItem(Element el, int pos) {
2277                int viewType = -1;
2278
2279                if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
2280                    if (el.getName().equals("note")) {
2281                        viewType = TYPE_NOTE;
2282                    } else if (el.getNamespace().equals("jabber:x:oob")) {
2283                        viewType = TYPE_WEB;
2284                    } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
2285                        viewType = TYPE_NOTE;
2286                    } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
2287                        Field field = mkField(el);
2288                        if (field != null) {
2289                            items.put(pos, field);
2290                            return field;
2291                        }
2292                    }
2293                } else if (response != null) {
2294                    viewType = TYPE_ERROR;
2295                }
2296
2297                Item item = new Item(el, viewType);
2298                items.put(pos, item);
2299                return item;
2300            }
2301
2302            class ActionsAdapter extends ArrayAdapter<Pair<String, String>> {
2303                protected Context ctx;
2304
2305                public ActionsAdapter(Context ctx) {
2306                    super(ctx, R.layout.simple_list_item);
2307                    this.ctx = ctx;
2308                }
2309
2310                @Override
2311                public View getView(int position, View convertView, ViewGroup parent) {
2312                    View v = super.getView(position, convertView, parent);
2313                    TextView tv = (TextView) v.findViewById(android.R.id.text1);
2314                    tv.setGravity(Gravity.CENTER);
2315                    tv.setText(getItem(position).second);
2316                    int resId = ctx.getResources().getIdentifier("action_" + getItem(position).first, "string" , ctx.getPackageName());
2317                    if (resId != 0 && getItem(position).second.equals(getItem(position).first)) tv.setText(ctx.getResources().getString(resId));
2318                    tv.setTextColor(ContextCompat.getColor(ctx, R.color.white));
2319                    tv.setBackgroundColor(UIHelper.getColorForName(getItem(position).first));
2320                    return v;
2321                }
2322
2323                public int getPosition(String s) {
2324                    for(int i = 0; i < getCount(); i++) {
2325                        if (getItem(i).first.equals(s)) return i;
2326                    }
2327                    return -1;
2328                }
2329
2330                public int countExceptCancel() {
2331                    int count = 0;
2332                    for(int i = 0; i < getCount(); i++) {
2333                        if (!getItem(i).first.equals("cancel")) count++;
2334                    }
2335                    return count;
2336                }
2337
2338                public void clearExceptCancel() {
2339                    Pair<String,String> cancelItem = null;
2340                    for(int i = 0; i < getCount(); i++) {
2341                        if (getItem(i).first.equals("cancel")) cancelItem = getItem(i);
2342                    }
2343                    clear();
2344                    if (cancelItem != null) add(cancelItem);
2345                }
2346            }
2347
2348            final int TYPE_ERROR = 1;
2349            final int TYPE_NOTE = 2;
2350            final int TYPE_WEB = 3;
2351            final int TYPE_RESULT_FIELD = 4;
2352            final int TYPE_TEXT_FIELD = 5;
2353            final int TYPE_CHECKBOX_FIELD = 6;
2354            final int TYPE_SPINNER_FIELD = 7;
2355            final int TYPE_RADIO_EDIT_FIELD = 8;
2356            final int TYPE_RESULT_CELL = 9;
2357            final int TYPE_PROGRESSBAR = 10;
2358            final int TYPE_SEARCH_LIST_FIELD = 11;
2359            final int TYPE_ITEM_CARD = 12;
2360            final int TYPE_BUTTON_GRID_FIELD = 13;
2361
2362            protected boolean loading = false;
2363            protected Timer loadingTimer = new Timer();
2364            protected String mTitle;
2365            protected String mNode;
2366            protected CommandPageBinding mBinding = null;
2367            protected IqPacket response = null;
2368            protected Element responseElement = null;
2369            protected List<Field> reported = null;
2370            protected SparseArray<Item> items = new SparseArray<>();
2371            protected XmppConnectionService xmppConnectionService;
2372            protected ActionsAdapter actionsAdapter;
2373            protected GridLayoutManager layoutManager;
2374            protected WebView actionToWebview = null;
2375            protected int fillableFieldCount = 0;
2376            protected IqPacket pendingResponsePacket = null;
2377
2378            CommandSession(String title, String node, XmppConnectionService xmppConnectionService) {
2379                loading();
2380                mTitle = title;
2381                mNode = node;
2382                this.xmppConnectionService = xmppConnectionService;
2383                if (mPager != null) setupLayoutManager();
2384                actionsAdapter = new ActionsAdapter(xmppConnectionService);
2385                actionsAdapter.registerDataSetObserver(new DataSetObserver() {
2386                    @Override
2387                    public void onChanged() {
2388                        if (mBinding == null) return;
2389
2390                        mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
2391                    }
2392
2393                    @Override
2394                    public void onInvalidated() {}
2395                });
2396            }
2397
2398            public String getTitle() {
2399                return mTitle;
2400            }
2401
2402            public void updateWithResponse(final IqPacket iq) {
2403                if (getView() != null && getView().isAttachedToWindow()) {
2404                    getView().post(() -> updateWithResponseUiThread(iq));
2405                } else {
2406                    pendingResponsePacket = iq;
2407                }
2408            }
2409
2410            protected void updateWithResponseUiThread(final IqPacket iq) {
2411                this.loadingTimer.cancel();
2412                this.loadingTimer = new Timer();
2413                this.loading = false;
2414                this.responseElement = null;
2415                this.fillableFieldCount = 0;
2416                this.reported = null;
2417                this.response = iq;
2418                this.items.clear();
2419                this.actionsAdapter.clear();
2420                layoutManager.setSpanCount(1);
2421
2422                boolean actionsCleared = false;
2423                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
2424                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
2425                    if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) {
2426                        xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true);
2427                    }
2428
2429                    for (Element el : command.getChildren()) {
2430                        if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
2431                            for (Element action : el.getChildren()) {
2432                                if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
2433                                if (action.getName().equals("execute")) continue;
2434
2435                                actionsAdapter.add(Pair.create(action.getName(), action.getName()));
2436                            }
2437                        }
2438                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
2439                            Data form = Data.parse(el);
2440                            String title = form.getTitle();
2441                            if (title != null) {
2442                                mTitle = title;
2443                                ConversationPagerAdapter.this.notifyDataSetChanged();
2444                            }
2445
2446                            if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
2447                                this.responseElement = el;
2448                                setupReported(el.findChild("reported", "jabber:x:data"));
2449                                if (mBinding != null) mBinding.form.setLayoutManager(setupLayoutManager());
2450                            }
2451
2452                            eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
2453                            if (actionList != null) {
2454                                actionsAdapter.clear();
2455
2456                                for (Option action : actionList.getOptions()) {
2457                                    actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
2458                                }
2459                            }
2460
2461                            String fillableFieldType = null;
2462                            String fillableFieldValue = null;
2463                            for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
2464                                if ((field.getType() == null || (!field.getType().equals("hidden") && !field.getType().equals("fixed"))) && field.getFieldName() != null && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
2465                                    fillableFieldType = field.getType();
2466                                    fillableFieldValue = field.getValue();
2467                                    fillableFieldCount++;
2468                                }
2469                            }
2470
2471                            if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 2 && fillableFieldType != null && (fillableFieldType.equals("list-single") || (fillableFieldType.equals("boolean") && fillableFieldValue == null))) {
2472                                actionsCleared = true;
2473                                actionsAdapter.clearExceptCancel();
2474                            }
2475                            break;
2476                        }
2477                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
2478                            String url = el.findChildContent("url", "jabber:x:oob");
2479                            if (url != null) {
2480                                String scheme = Uri.parse(url).getScheme();
2481                                if (scheme.equals("http") || scheme.equals("https")) {
2482                                    this.responseElement = el;
2483                                    break;
2484                                }
2485                                if (scheme.equals("xmpp")) {
2486                                    final Intent intent = new Intent(getView().getContext(), UriHandlerActivity.class);
2487                                    intent.setAction(Intent.ACTION_VIEW);
2488                                    intent.setData(Uri.parse(url));
2489                                    getView().getContext().startActivity(intent);
2490                                    break;
2491                                }
2492                            }
2493                        }
2494                        if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
2495                            this.responseElement = el;
2496                            break;
2497                        }
2498                    }
2499
2500                    if (responseElement == null && command.getAttribute("status") != null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
2501                        if (mNode.equals("jabber:iq:register") && command.getAttribute("status").equals("canceled")) {
2502                            if (xmppConnectionService.isOnboarding()) {
2503                                if (!xmppConnectionService.getPreferences().contains("onboarding_action")) {
2504                                    xmppConnectionService.getPreferences().edit().putString("onboarding_action", "cancel").commit();
2505                                }
2506                                xmppConnectionService.deleteAccount(getAccount());
2507                            }
2508                            xmppConnectionService.archiveConversation(Conversation.this);
2509                        }
2510
2511                        removeSession(this);
2512                        return;
2513                    }
2514
2515                    if (command.getAttribute("status").equals("executing") && actionsAdapter.countExceptCancel() < 1 && !actionsCleared) {
2516                        // No actions have been given, but we are not done?
2517                        // This is probably a spec violation, but we should do *something*
2518                        actionsAdapter.add(Pair.create("execute", "execute"));
2519                    }
2520
2521                    if (!actionsAdapter.isEmpty() || fillableFieldCount > 0) {
2522                        if (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled")) {
2523                            actionsAdapter.add(Pair.create("close", "close"));
2524                        } else if (actionsAdapter.getPosition("cancel") < 0) {
2525                            actionsAdapter.insert(Pair.create("cancel", "cancel"), 0);
2526                        }
2527                    }
2528                }
2529
2530                if (actionsAdapter.isEmpty()) {
2531                    actionsAdapter.add(Pair.create("close", "close"));
2532                }
2533
2534                Data dataForm = null;
2535                if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
2536                if (mNode.equals("jabber:iq:register") &&
2537                    xmppConnectionService.getPreferences().contains("onboarding_action") &&
2538                    dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
2539
2540
2541                    dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
2542                    execute();
2543                }
2544                xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
2545                notifyDataSetChanged();
2546            }
2547
2548            protected void setupReported(Element el) {
2549                if (el == null) {
2550                    reported = null;
2551                    return;
2552                }
2553
2554                reported = new ArrayList<>();
2555                for (Element fieldEl : el.getChildren()) {
2556                    if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
2557                    reported.add(mkField(fieldEl));
2558                }
2559            }
2560
2561            @Override
2562            public int getItemCount() {
2563                if (loading) return 1;
2564                if (response == null) return 0;
2565                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
2566                    int i = 0;
2567                    for (Element el : responseElement.getChildren()) {
2568                        if (!el.getNamespace().equals("jabber:x:data")) continue;
2569                        if (el.getName().equals("title")) continue;
2570                        if (el.getName().equals("field")) {
2571                            String type = el.getAttribute("type");
2572                            if (type != null && type.equals("hidden")) continue;
2573                            if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2574                        }
2575
2576                        if (el.getName().equals("reported") || el.getName().equals("item")) {
2577                            if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2578                                if (el.getName().equals("reported")) continue;
2579                                i += 1;
2580                            } else {
2581                                if (reported != null) i += reported.size();
2582                            }
2583                            continue;
2584                        }
2585
2586                        i++;
2587                    }
2588                    return i;
2589                }
2590                return 1;
2591            }
2592
2593            public Item getItem(int position) {
2594                if (loading) return new Item(null, TYPE_PROGRESSBAR);
2595                if (items.get(position) != null) return items.get(position);
2596                if (response == null) return null;
2597
2598                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
2599                    if (responseElement.getNamespace().equals("jabber:x:data")) {
2600                        int i = 0;
2601                        for (Element el : responseElement.getChildren()) {
2602                            if (!el.getNamespace().equals("jabber:x:data")) continue;
2603                            if (el.getName().equals("title")) continue;
2604                            if (el.getName().equals("field")) {
2605                                String type = el.getAttribute("type");
2606                                if (type != null && type.equals("hidden")) continue;
2607                                if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2608                            }
2609
2610                            if (el.getName().equals("reported") || el.getName().equals("item")) {
2611                                Cell cell = null;
2612
2613                                if (reported != null) {
2614                                    if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2615                                        if (el.getName().equals("reported")) continue;
2616                                        if (i == position) {
2617                                            items.put(position, new Item(el, TYPE_ITEM_CARD));
2618                                            return items.get(position);
2619                                        }
2620                                    } else {
2621                                        if (reported.size() > position - i) {
2622                                            Field reportedField = reported.get(position - i);
2623                                            Element itemField = null;
2624                                            if (el.getName().equals("item")) {
2625                                                for (Element subel : el.getChildren()) {
2626                                                    if (subel.getAttribute("var").equals(reportedField.getVar())) {
2627                                                       itemField = subel;
2628                                                       break;
2629                                                    }
2630                                                }
2631                                            }
2632                                            cell = new Cell(reportedField, itemField);
2633                                        } else {
2634                                            i += reported.size();
2635                                            continue;
2636                                        }
2637                                    }
2638                                }
2639
2640                                if (cell != null) {
2641                                    items.put(position, cell);
2642                                    return cell;
2643                                }
2644                            }
2645
2646                            if (i < position) {
2647                                i++;
2648                                continue;
2649                            }
2650
2651                            return mkItem(el, position);
2652                        }
2653                    }
2654                }
2655
2656                return mkItem(responseElement == null ? response : responseElement, position);
2657            }
2658
2659            @Override
2660            public int getItemViewType(int position) {
2661                return getItem(position).viewType;
2662            }
2663
2664            @Override
2665            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2666                switch(viewType) {
2667                    case TYPE_ERROR: {
2668                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2669                        return new ErrorViewHolder(binding);
2670                    }
2671                    case TYPE_NOTE: {
2672                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2673                        return new NoteViewHolder(binding);
2674                    }
2675                    case TYPE_WEB: {
2676                        CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2677                        return new WebViewHolder(binding);
2678                    }
2679                    case TYPE_RESULT_FIELD: {
2680                        CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2681                        return new ResultFieldViewHolder(binding);
2682                    }
2683                    case TYPE_RESULT_CELL: {
2684                        CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2685                        return new ResultCellViewHolder(binding);
2686                    }
2687                    case TYPE_ITEM_CARD: {
2688                        CommandItemCardBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_item_card, container, false);
2689                        return new ItemCardViewHolder(binding);
2690                    }
2691                    case TYPE_CHECKBOX_FIELD: {
2692                        CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2693                        return new CheckboxFieldViewHolder(binding);
2694                    }
2695                    case TYPE_SEARCH_LIST_FIELD: {
2696                        CommandSearchListFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_search_list_field, container, false);
2697                        return new SearchListFieldViewHolder(binding);
2698                    }
2699                    case TYPE_RADIO_EDIT_FIELD: {
2700                        CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2701                        return new RadioEditFieldViewHolder(binding);
2702                    }
2703                    case TYPE_SPINNER_FIELD: {
2704                        CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2705                        return new SpinnerFieldViewHolder(binding);
2706                    }
2707                    case TYPE_BUTTON_GRID_FIELD: {
2708                        CommandButtonGridFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_button_grid_field, container, false);
2709                        return new ButtonGridFieldViewHolder(binding);
2710                    }
2711                    case TYPE_TEXT_FIELD: {
2712                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2713                        return new TextFieldViewHolder(binding);
2714                    }
2715                    case TYPE_PROGRESSBAR: {
2716                        CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
2717                        return new ProgressBarViewHolder(binding);
2718                    }
2719                    default:
2720                        throw new IllegalArgumentException("Unknown viewType: " + viewType);
2721                }
2722            }
2723
2724            @Override
2725            public void onBindViewHolder(ViewHolder viewHolder, int position) {
2726                viewHolder.bind(getItem(position));
2727            }
2728
2729            public View getView() {
2730                if (mBinding == null) return null;
2731                return mBinding.getRoot();
2732            }
2733
2734            public boolean validate() {
2735                int count = getItemCount();
2736                boolean isValid = true;
2737                for (int i = 0; i < count; i++) {
2738                    boolean oneIsValid = getItem(i).validate();
2739                    isValid = isValid && oneIsValid;
2740                }
2741                notifyDataSetChanged();
2742                return isValid;
2743            }
2744
2745            public boolean execute() {
2746                return execute("execute");
2747            }
2748
2749            public boolean execute(int actionPosition) {
2750                return execute(actionsAdapter.getItem(actionPosition).first);
2751            }
2752
2753            public boolean execute(String action) {
2754                if (!action.equals("cancel") && !action.equals("prev") && !validate()) return false;
2755
2756                if (response == null) return true;
2757                Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2758                if (command == null) return true;
2759                String status = command.getAttribute("status");
2760                if (status == null || (!status.equals("executing") && !action.equals("prev"))) return true;
2761
2762                if (actionToWebview != null && !action.equals("cancel")) {
2763                    actionToWebview.postWebMessage(new WebMessage("xmpp_xep0050/" + action), Uri.parse("*"));
2764                    return false;
2765                }
2766
2767                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2768                packet.setTo(response.getFrom());
2769                final Element c = packet.addChild("command", Namespace.COMMANDS);
2770                c.setAttribute("node", mNode);
2771                c.setAttribute("sessionid", command.getAttribute("sessionid"));
2772
2773                String formType = responseElement == null ? null : responseElement.getAttribute("type");
2774                if (!action.equals("cancel") &&
2775                    !action.equals("prev") &&
2776                    responseElement != null &&
2777                    responseElement.getName().equals("x") &&
2778                    responseElement.getNamespace().equals("jabber:x:data") &&
2779                    formType != null && formType.equals("form")) {
2780
2781                    Data form = Data.parse(responseElement);
2782                    eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
2783                    if (actionList != null) {
2784                        actionList.setValue(action);
2785                        c.setAttribute("action", "execute");
2786                    }
2787
2788                    if (mNode.equals("jabber:iq:register") && xmppConnectionService.isOnboarding() && form.getValue("gateway-jid") != null) {
2789                        xmppConnectionService.getPreferences().edit().putString("onboarding_action", form.getValue("gateway-jid")).commit();
2790                    }
2791
2792                    responseElement.setAttribute("type", "submit");
2793                    Element rsm = responseElement.findChild("set", "http://jabber.org/protocol/rsm");
2794                    if (rsm != null) {
2795                        Element max = new Element("max", "http://jabber.org/protocol/rsm");
2796                        max.setContent("1000");
2797                        rsm.addChild(max);
2798                    }
2799
2800                    c.addChild(responseElement);
2801                }
2802
2803                if (c.getAttribute("action") == null) c.setAttribute("action", action);
2804
2805                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2806                    updateWithResponse(iq);
2807                });
2808
2809                loading();
2810                return false;
2811            }
2812
2813            protected void loading() {
2814                loadingTimer.schedule(new TimerTask() {
2815                    @Override
2816                    public void run() {
2817                        getView().post(() -> {
2818                            loading = true;
2819                            notifyDataSetChanged();
2820                        });
2821                    }
2822                }, 500);
2823            }
2824
2825            protected GridLayoutManager setupLayoutManager() {
2826                int spanCount = 1;
2827
2828                if (reported != null && mPager != null) {
2829                    float screenWidth = mPager.getContext().getResources().getDisplayMetrics().widthPixels;
2830                    TextPaint paint = ((TextView) LayoutInflater.from(mPager.getContext()).inflate(R.layout.command_result_cell, null)).getPaint();
2831                    float tableHeaderWidth = reported.stream().reduce(
2832                        0f,
2833                        (total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------"), paint),
2834                        (a, b) -> a + b
2835                    );
2836
2837                    spanCount = tableHeaderWidth > 0.65 * screenWidth ? 1 : this.reported.size();
2838                }
2839
2840                if (layoutManager != null && layoutManager.getSpanCount() != spanCount) {
2841                    items.clear();
2842                    notifyDataSetChanged();
2843                }
2844
2845                layoutManager = new GridLayoutManager(mPager.getContext(), spanCount);
2846                layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2847                    @Override
2848                    public int getSpanSize(int position) {
2849                        if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2850                        return 1;
2851                    }
2852                });
2853                return layoutManager;
2854            }
2855
2856            public void setBinding(CommandPageBinding b) {
2857                mBinding = b;
2858                // https://stackoverflow.com/a/32350474/8611
2859                mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
2860                    @Override
2861                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
2862                        if(rv.getChildCount() > 0) {
2863                            int[] location = new int[2];
2864                            rv.getLocationOnScreen(location);
2865                            View childView = rv.findChildViewUnder(e.getX(), e.getY());
2866                            if (childView instanceof ViewGroup) {
2867                                childView = findViewAt((ViewGroup) childView, location[0] + e.getX(), location[1] + e.getY());
2868                            }
2869                            int action = e.getAction();
2870                            switch (action) {
2871                                case MotionEvent.ACTION_DOWN:
2872                                    if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(1)) || childView instanceof WebView) {
2873                                        rv.requestDisallowInterceptTouchEvent(true);
2874                                    }
2875                                case MotionEvent.ACTION_UP:
2876                                    if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(-11)) || childView instanceof WebView) {
2877                                        rv.requestDisallowInterceptTouchEvent(true);
2878                                    }
2879                            }
2880                        }
2881
2882                        return false;
2883                    }
2884
2885                    @Override
2886                    public void onRequestDisallowInterceptTouchEvent(boolean disallow) { }
2887
2888                    @Override
2889                    public void onTouchEvent(RecyclerView rv, MotionEvent e) { }
2890                });
2891                mBinding.form.setLayoutManager(setupLayoutManager());
2892                mBinding.form.setAdapter(this);
2893                mBinding.actions.setAdapter(actionsAdapter);
2894                mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2895                    if (execute(pos)) {
2896                        removeSession(CommandSession.this);
2897                    }
2898                });
2899
2900                actionsAdapter.notifyDataSetChanged();
2901
2902                if (pendingResponsePacket != null) {
2903                    final IqPacket pending = pendingResponsePacket;
2904                    pendingResponsePacket = null;
2905                    updateWithResponseUiThread(pending);
2906                }
2907            }
2908
2909            // https://stackoverflow.com/a/36037991/8611
2910            private View findViewAt(ViewGroup viewGroup, float x, float y) {
2911                for(int i = 0; i < viewGroup.getChildCount(); i++) {
2912                    View child = viewGroup.getChildAt(i);
2913                    if (child instanceof ViewGroup && !(child instanceof AbsListView) && !(child instanceof WebView)) {
2914                        View foundView = findViewAt((ViewGroup) child, x, y);
2915                        if (foundView != null && foundView.isShown()) {
2916                            return foundView;
2917                        }
2918                    } else {
2919                        int[] location = new int[2];
2920                        child.getLocationOnScreen(location);
2921                        Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight());
2922                        if (rect.contains((int)x, (int)y)) {
2923                            return child;
2924                        }
2925                    }
2926                }
2927
2928                return null;
2929            }
2930        }
2931    }
2932}