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;
 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 boolean switchToSession(final String node) {
1307        return pagerAdapter.switchToSession(node);
1308    }
1309
1310    public void setupViewPager(ViewPager pager, TabLayout tabs, boolean onboarding) {
1311        pagerAdapter.setupViewPager(pager, tabs, onboarding);
1312    }
1313
1314    public void showViewPager() {
1315        pagerAdapter.show();
1316    }
1317
1318    public void hideViewPager() {
1319        pagerAdapter.hide();
1320    }
1321
1322    public interface OnMessageFound {
1323        void onMessageFound(final Message message);
1324    }
1325
1326    public static class Draft {
1327        private final String message;
1328        private final long timestamp;
1329
1330        private Draft(String message, long timestamp) {
1331            this.message = message;
1332            this.timestamp = timestamp;
1333        }
1334
1335        public long getTimestamp() {
1336            return timestamp;
1337        }
1338
1339        public String getMessage() {
1340            return message;
1341        }
1342    }
1343
1344    public class ConversationPagerAdapter extends PagerAdapter {
1345        protected ViewPager mPager = null;
1346        protected TabLayout mTabs = null;
1347        ArrayList<CommandSession> sessions = null;
1348        protected View page1 = null;
1349        protected View page2 = null;
1350        protected boolean mOnboarding = false;
1351
1352        public void setupViewPager(ViewPager pager, TabLayout tabs, boolean onboarding) {
1353            mPager = pager;
1354            mTabs = tabs;
1355            mOnboarding = onboarding;
1356
1357            if (mPager == null) return;
1358            if (sessions != null) show();
1359
1360            page1 = pager.getChildAt(0) == null ? page1 : pager.getChildAt(0);
1361            page2 = pager.getChildAt(1) == null ? page2 : pager.getChildAt(1);
1362            pager.setAdapter(this);
1363            tabs.setupWithViewPager(mPager);
1364            pager.post(() -> pager.setCurrentItem(getCurrentTab()));
1365
1366            mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1367                public void onPageScrollStateChanged(int state) { }
1368                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1369
1370                public void onPageSelected(int position) {
1371                    setCurrentTab(position);
1372                }
1373            });
1374        }
1375
1376        public void show() {
1377            if (sessions == null) {
1378                sessions = new ArrayList<>();
1379                notifyDataSetChanged();
1380            }
1381            if (!mOnboarding && mTabs != null) mTabs.setVisibility(View.VISIBLE);
1382        }
1383
1384        public void hide() {
1385            if (sessions != null && !sessions.isEmpty()) return; // Do not hide during active session
1386            if (mPager != null) mPager.setCurrentItem(0);
1387            if (mTabs != null) mTabs.setVisibility(View.GONE);
1388            sessions = null;
1389            notifyDataSetChanged();
1390        }
1391
1392        public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1393            show();
1394            CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
1395
1396            final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1397            packet.setTo(command.getAttributeAsJid("jid"));
1398            final Element c = packet.addChild("command", Namespace.COMMANDS);
1399            c.setAttribute("node", command.getAttribute("node"));
1400            c.setAttribute("action", "execute");
1401
1402            final TimerTask task = new TimerTask() {
1403                @Override
1404                public void run() {
1405                    if (getAccount().getStatus() != Account.State.ONLINE) {
1406                        new Timer().schedule(this, 1000);
1407                    } else {
1408                        xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1409                            session.updateWithResponse(iq);
1410                        });
1411                    }
1412                }
1413            };
1414
1415            if (command.getAttribute("node").equals("jabber:iq:register") && packet.getTo().asBareJid().equals(Jid.of("cheogram.com"))) {
1416                new com.cheogram.android.CheogramLicenseChecker(mPager.getContext(), (signedData, signature) -> {
1417                    if (signedData != null && signature != null) {
1418                        c.addChild("license", "https://ns.cheogram.com/google-play").setContent(signedData);
1419                        c.addChild("licenseSignature", "https://ns.cheogram.com/google-play").setContent(signature);
1420                    }
1421
1422                    task.run();
1423                }).checkLicense();
1424            } else {
1425                task.run();
1426            }
1427
1428            sessions.add(session);
1429            notifyDataSetChanged();
1430            if (mPager != null) mPager.setCurrentItem(getCount() - 1);
1431        }
1432
1433        public void removeSession(CommandSession session) {
1434            sessions.remove(session);
1435            notifyDataSetChanged();
1436        }
1437
1438        public boolean switchToSession(final String node) {
1439            if (sessions == null) return false;
1440
1441            int i = 0;
1442            for (CommandSession session : sessions) {
1443                if (session.mNode.equals(node)) {
1444                    if (mPager != null) mPager.setCurrentItem(i + 2);
1445                    return true;
1446                }
1447                i++;
1448            }
1449
1450            return false;
1451        }
1452
1453        @NonNull
1454        @Override
1455        public Object instantiateItem(@NonNull ViewGroup container, int position) {
1456            if (position == 0) {
1457                if (page1.getParent() == null) container.addView(page1);
1458                return page1;
1459            }
1460            if (position == 1) {
1461                if (page2.getParent() == null) container.addView(page2);
1462                return page2;
1463            }
1464
1465            CommandSession session = sessions.get(position-2);
1466            CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
1467            container.addView(binding.getRoot());
1468            session.setBinding(binding);
1469            return session;
1470        }
1471
1472        @Override
1473        public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1474            if (position < 2) return;
1475
1476            container.removeView(((CommandSession) o).getView());
1477        }
1478
1479        @Override
1480        public int getItemPosition(Object o) {
1481            if (mPager != null) {
1482                if (o == page1) return PagerAdapter.POSITION_UNCHANGED;
1483                if (o == page2) return PagerAdapter.POSITION_UNCHANGED;
1484            }
1485
1486            int pos = sessions == null ? -1 : sessions.indexOf(o);
1487            if (pos < 0) return PagerAdapter.POSITION_NONE;
1488            return pos + 2;
1489        }
1490
1491        @Override
1492        public int getCount() {
1493            if (sessions == null) return 1;
1494
1495            int count = 2 + sessions.size();
1496            if (mTabs == null) return count;
1497
1498            if (count > 2) {
1499                mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1500            } else {
1501                mTabs.setTabMode(TabLayout.MODE_FIXED);
1502            }
1503            return count;
1504        }
1505
1506        @Override
1507        public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1508            if (view == o) return true;
1509
1510            if (o instanceof CommandSession) {
1511                return ((CommandSession) o).getView() == view;
1512            }
1513
1514            return false;
1515        }
1516
1517        @Nullable
1518        @Override
1519        public CharSequence getPageTitle(int position) {
1520            switch (position) {
1521                case 0:
1522                    return "Conversation";
1523                case 1:
1524                    return "Commands";
1525                default:
1526                    CommandSession session = sessions.get(position-2);
1527                    if (session == null) return super.getPageTitle(position);
1528                    return session.getTitle();
1529            }
1530        }
1531
1532        class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
1533            abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1534                protected T binding;
1535
1536                public ViewHolder(T binding) {
1537                    super(binding.getRoot());
1538                    this.binding = binding;
1539                }
1540
1541                abstract public void bind(Item el);
1542
1543                protected void setTextOrHide(TextView v, Optional<String> s) {
1544                    if (s == null || !s.isPresent()) {
1545                        v.setVisibility(View.GONE);
1546                    } else {
1547                        v.setVisibility(View.VISIBLE);
1548                        v.setText(s.get());
1549                    }
1550                }
1551
1552                protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
1553                    int flags = 0;
1554                    if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
1555                    textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1556
1557                    String type = field.getAttribute("type");
1558                    if (type != null) {
1559                        if (type.equals("text-multi") || type.equals("jid-multi")) {
1560                            flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
1561                        }
1562
1563                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1564
1565                        if (type.equals("jid-single") || type.equals("jid-multi")) {
1566                            textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1567                        }
1568
1569                        if (type.equals("text-private")) {
1570                            textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1571                            if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
1572                        }
1573                    }
1574
1575                    Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1576                    if (validate == null) return;
1577                    String datatype = validate.getAttribute("datatype");
1578                    if (datatype == null) return;
1579
1580                    if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
1581                        textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
1582                    }
1583
1584                    if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
1585                        textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
1586                    }
1587
1588                    if (datatype.equals("xs:date")) {
1589                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
1590                    }
1591
1592                    if (datatype.equals("xs:dateTime")) {
1593                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
1594                    }
1595
1596                    if (datatype.equals("xs:time")) {
1597                        textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
1598                    }
1599
1600                    if (datatype.equals("xs:anyURI")) {
1601                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
1602                    }
1603
1604                    if (datatype.equals("html:tel")) {
1605                        textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
1606                    }
1607
1608                    if (datatype.equals("html:email")) {
1609                        textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1610                    }
1611                }
1612            }
1613
1614            class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1615                public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1616
1617                @Override
1618                public void bind(Item iq) {
1619                    binding.errorIcon.setVisibility(View.VISIBLE);
1620
1621                    Element error = iq.el.findChild("error");
1622                    if (error == null) return;
1623                    String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1624                    if (text == null || text.equals("")) {
1625                        text = error.getChildren().get(0).getName();
1626                    }
1627                    binding.message.setText(text);
1628                }
1629            }
1630
1631            class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1632                public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1633
1634                @Override
1635                public void bind(Item note) {
1636                    binding.message.setText(note.el.getContent());
1637
1638                    String type = note.el.getAttribute("type");
1639                    if (type != null && type.equals("error")) {
1640                        binding.errorIcon.setVisibility(View.VISIBLE);
1641                    }
1642                }
1643            }
1644
1645            class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1646                public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1647
1648                @Override
1649                public void bind(Item item) {
1650                    Field field = (Field) item;
1651                    setTextOrHide(binding.label, field.getLabel());
1652                    setTextOrHide(binding.desc, field.getDesc());
1653
1654                    ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
1655                    for (Element el : field.el.getChildren()) {
1656                        if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1657                            values.add(el.getContent());
1658                        }
1659                    }
1660                    binding.values.setAdapter(values);
1661
1662                    if (field.getType().equals(Optional.of("jid-single")) || field.getType().equals(Optional.of("jid-multi"))) {
1663                        binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
1664                            new FixedURLSpan("xmpp:" + Jid.ofEscaped(values.getItem(pos)).toEscapedString()).onClick(binding.values);
1665                        });
1666                    }
1667
1668                    binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
1669                        if (ShareUtil.copyTextToClipboard(binding.getRoot().getContext(), values.getItem(pos), R.string.message)) {
1670                            Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1671                        }
1672                        return true;
1673                    });
1674                }
1675            }
1676
1677            class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
1678                public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
1679
1680                @Override
1681                public void bind(Item item) {
1682                    Cell cell = (Cell) item;
1683
1684                    if (cell.el == null) {
1685                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
1686                        setTextOrHide(binding.text, cell.reported.getLabel());
1687                    } else {
1688                        String value = cell.el.findChildContent("value", "jabber:x:data");
1689                        SpannableStringBuilder text = new SpannableStringBuilder(value == null ? "" : value);
1690                        if (cell.reported.getType().equals(Optional.of("jid-single"))) {
1691                            text.setSpan(new FixedURLSpan("xmpp:" + Jid.ofEscaped(text.toString()).toEscapedString()), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1692                        }
1693
1694                        binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
1695                        binding.text.setText(text);
1696
1697                        BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance();
1698                        method.setOnLinkLongClickListener((tv, url) -> {
1699                            tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
1700                            ShareUtil.copyLinkToClipboard(binding.getRoot().getContext(), url);
1701                            return true;
1702                        });
1703                        binding.text.setMovementMethod(method);
1704                    }
1705                }
1706            }
1707
1708            class ItemCardViewHolder extends ViewHolder<CommandItemCardBinding> {
1709                public ItemCardViewHolder(CommandItemCardBinding binding) { super(binding); }
1710
1711                @Override
1712                public void bind(Item item) {
1713                    binding.fields.removeAllViews();
1714
1715                    for (Field field : reported) {
1716                        CommandResultFieldBinding row = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.command_result_field, binding.fields, false);
1717                        GridLayout.LayoutParams param = new GridLayout.LayoutParams();
1718                        param.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f);
1719                        param.width = 0;
1720                        row.getRoot().setLayoutParams(param);
1721                        binding.fields.addView(row.getRoot());
1722                        for (Element el : item.el.getChildren()) {
1723                            if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data") && el.getAttribute("var") != null && el.getAttribute("var").equals(field.getVar())) {
1724                                for (String label : field.getLabel().asSet()) {
1725                                    el.setAttribute("label", label);
1726                                }
1727                                for (String desc : field.getDesc().asSet()) {
1728                                    el.setAttribute("desc", desc);
1729                                }
1730                                for (String type : field.getType().asSet()) {
1731                                    el.setAttribute("type", type);
1732                                }
1733                                Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1734                                if (validate != null) el.addChild(validate);
1735                                new ResultFieldViewHolder(row).bind(new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), -1));
1736                            }
1737                        }
1738                    }
1739                }
1740            }
1741
1742            class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
1743                public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
1744                    super(binding);
1745                    binding.row.setOnClickListener((v) -> {
1746                        binding.checkbox.toggle();
1747                    });
1748                    binding.checkbox.setOnCheckedChangeListener(this);
1749                }
1750                protected Element mValue = null;
1751
1752                @Override
1753                public void bind(Item item) {
1754                    Field field = (Field) item;
1755                    binding.label.setText(field.getLabel().or(""));
1756                    setTextOrHide(binding.desc, field.getDesc());
1757                    mValue = field.getValue();
1758                    binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
1759                }
1760
1761                @Override
1762                public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
1763                    if (mValue == null) return;
1764
1765                    mValue.setContent(isChecked ? "true" : "false");
1766                }
1767            }
1768
1769            class SearchListFieldViewHolder extends ViewHolder<CommandSearchListFieldBinding> implements TextWatcher {
1770                public SearchListFieldViewHolder(CommandSearchListFieldBinding binding) {
1771                    super(binding);
1772                    binding.search.addTextChangedListener(this);
1773                }
1774                protected Element mValue = null;
1775                List<Option> options = new ArrayList<>();
1776                protected ArrayAdapter<Option> adapter;
1777                protected boolean open;
1778
1779                @Override
1780                public void bind(Item item) {
1781                    Field field = (Field) item;
1782                    setTextOrHide(binding.label, field.getLabel());
1783                    setTextOrHide(binding.desc, field.getDesc());
1784
1785                    if (field.error != null) {
1786                        binding.desc.setVisibility(View.VISIBLE);
1787                        binding.desc.setText(field.error);
1788                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1789                    } else {
1790                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1791                    }
1792
1793                    mValue = field.getValue();
1794
1795                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1796                    open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
1797                    setupInputType(field.el, binding.search, null);
1798
1799                    options = field.getOptions();
1800                    binding.list.setOnItemClickListener((parent, view, position, id) -> {
1801                        mValue.setContent(adapter.getItem(binding.list.getCheckedItemPosition()).getValue());
1802                        if (open) binding.search.setText(mValue.getContent());
1803                    });
1804                    search("");
1805                }
1806
1807                @Override
1808                public void afterTextChanged(Editable s) {
1809                    if (open) mValue.setContent(s.toString());
1810                    search(s.toString());
1811                }
1812
1813                @Override
1814                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1815
1816                @Override
1817                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1818
1819                protected void search(String s) {
1820                    List<Option> filteredOptions;
1821                    final String q = s.replaceAll("\\W", "").toLowerCase();
1822                    if (q == null || q.equals("")) {
1823                        filteredOptions = options;
1824                    } else {
1825                        filteredOptions = options.stream().filter(o -> o.toString().replaceAll("\\W", "").toLowerCase().contains(q)).collect(Collectors.toList());
1826                    }
1827                    adapter = new ArrayAdapter(binding.getRoot().getContext(), R.layout.simple_list_item, filteredOptions);
1828                    binding.list.setAdapter(adapter);
1829
1830                    int checkedPos = filteredOptions.indexOf(new Option(mValue.getContent(), ""));
1831                    if (checkedPos >= 0) binding.list.setItemChecked(checkedPos, true);
1832                }
1833            }
1834
1835            class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
1836                public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
1837                    super(binding);
1838                    binding.open.addTextChangedListener(this);
1839                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
1840                        @Override
1841                        public View getView(int position, View convertView, ViewGroup parent) {
1842                            CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
1843                            v.setId(position);
1844                            v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
1845                            v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
1846                            return v;
1847                        }
1848                    };
1849                }
1850                protected Element mValue = null;
1851                protected ArrayAdapter<Option> options;
1852
1853                @Override
1854                public void bind(Item item) {
1855                    Field field = (Field) item;
1856                    setTextOrHide(binding.label, field.getLabel());
1857                    setTextOrHide(binding.desc, field.getDesc());
1858
1859                    if (field.error != null) {
1860                        binding.desc.setVisibility(View.VISIBLE);
1861                        binding.desc.setText(field.error);
1862                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1863                    } else {
1864                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1865                    }
1866
1867                    mValue = field.getValue();
1868
1869                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1870                    binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
1871                    binding.open.setText(mValue.getContent());
1872                    setupInputType(field.el, binding.open, null);
1873
1874                    options.clear();
1875                    List<Option> theOptions = field.getOptions();
1876                    options.addAll(theOptions);
1877
1878                    float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
1879                    TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
1880                    float maxColumnWidth = theOptions.stream().map((x) ->
1881                        StaticLayout.getDesiredWidth(x.toString(), paint)
1882                    ).max(Float::compare).orElse(new Float(0.0));
1883                    if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
1884                        binding.radios.setNumColumns(theOptions.size());
1885                    } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
1886                        binding.radios.setNumColumns(theOptions.size() / 2);
1887                    } else {
1888                        binding.radios.setNumColumns(1);
1889                    }
1890                    binding.radios.setAdapter(options);
1891                }
1892
1893                @Override
1894                public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
1895                    if (mValue == null) return;
1896
1897                    if (isChecked) {
1898                        mValue.setContent(options.getItem(radio.getId()).getValue());
1899                        binding.open.setText(mValue.getContent());
1900                    }
1901                    options.notifyDataSetChanged();
1902                }
1903
1904                @Override
1905                public void afterTextChanged(Editable s) {
1906                    if (mValue == null) return;
1907
1908                    mValue.setContent(s.toString());
1909                    options.notifyDataSetChanged();
1910                }
1911
1912                @Override
1913                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1914
1915                @Override
1916                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1917            }
1918
1919            class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
1920                public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
1921                    super(binding);
1922                    binding.spinner.setOnItemSelectedListener(this);
1923                }
1924                protected Element mValue = null;
1925
1926                @Override
1927                public void bind(Item item) {
1928                    Field field = (Field) item;
1929                    setTextOrHide(binding.label, field.getLabel());
1930                    binding.spinner.setPrompt(field.getLabel().or(""));
1931                    setTextOrHide(binding.desc, field.getDesc());
1932
1933                    mValue = field.getValue();
1934
1935                    ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
1936                    options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1937                    options.addAll(field.getOptions());
1938
1939                    binding.spinner.setAdapter(options);
1940                    binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
1941                }
1942
1943                @Override
1944                public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
1945                    Option o = (Option) parent.getItemAtPosition(pos);
1946                    if (mValue == null) return;
1947
1948                    mValue.setContent(o == null ? "" : o.getValue());
1949                }
1950
1951                @Override
1952                public void onNothingSelected(AdapterView<?> parent) {
1953                    mValue.setContent("");
1954                }
1955            }
1956
1957            class ButtonGridFieldViewHolder extends ViewHolder<CommandButtonGridFieldBinding> {
1958                public ButtonGridFieldViewHolder(CommandButtonGridFieldBinding binding) {
1959                    super(binding);
1960                    options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.button_grid_item) {
1961                        @Override
1962                        public View getView(int position, View convertView, ViewGroup parent) {
1963                            Button v = (Button) super.getView(position, convertView, parent);
1964                            v.setOnClickListener((view) -> {
1965                                loading = true;
1966                                mValue.setContent(getItem(position).getValue());
1967                                execute();
1968                            });
1969
1970                            final SVG icon = getItem(position).getIcon();
1971                            if (icon != null) {
1972                                 v.post(() -> {
1973                                     if (v.getHeight() == 0) return;
1974                                     icon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
1975                                     Bitmap bitmap = Bitmap.createBitmap(v.getHeight(), v.getHeight(), Bitmap.Config.ARGB_8888);
1976                                     Canvas bmcanvas = new Canvas(bitmap);
1977                                     icon.renderToCanvas(bmcanvas);
1978                                     v.setCompoundDrawablesRelativeWithIntrinsicBounds(new BitmapDrawable(bitmap), null, null, null);
1979                                 });
1980                            }
1981
1982                            return v;
1983                        }
1984                    };
1985                }
1986                protected Element mValue = null;
1987                protected ArrayAdapter<Option> options;
1988                protected Option defaultOption = null;
1989
1990                @Override
1991                public void bind(Item item) {
1992                    Field field = (Field) item;
1993                    setTextOrHide(binding.label, field.getLabel());
1994                    setTextOrHide(binding.desc, field.getDesc());
1995
1996                    if (field.error != null) {
1997                        binding.desc.setVisibility(View.VISIBLE);
1998                        binding.desc.setText(field.error);
1999                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
2000                    } else {
2001                        binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
2002                    }
2003
2004                    mValue = field.getValue();
2005
2006                    Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2007                    binding.openButton.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
2008                    binding.openButton.setOnClickListener((view) -> {
2009                        AlertDialog.Builder builder = new AlertDialog.Builder(binding.getRoot().getContext());
2010                        DialogQuickeditBinding dialogBinding = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.dialog_quickedit, null, false);
2011                        builder.setPositiveButton(R.string.action_execute, null);
2012                        if (field.getDesc().isPresent()) {
2013                            dialogBinding.inputLayout.setHint(field.getDesc().get());
2014                        }
2015                        dialogBinding.inputEditText.requestFocus();
2016                        dialogBinding.inputEditText.getText().append(mValue.getContent());
2017                        builder.setView(dialogBinding.getRoot());
2018                        builder.setNegativeButton(R.string.cancel, null);
2019                        final AlertDialog dialog = builder.create();
2020                        dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(dialogBinding.inputEditText));
2021                        dialog.show();
2022                        View.OnClickListener clickListener = v -> {
2023                            loading = true;
2024                            String value = dialogBinding.inputEditText.getText().toString();
2025                            mValue.setContent(value);
2026                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2027                            dialog.dismiss();
2028                            execute();
2029                        };
2030                        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
2031                        dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
2032                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2033                            dialog.dismiss();
2034                        }));
2035                        dialog.setCanceledOnTouchOutside(false);
2036                        dialog.setOnDismissListener(dialog1 -> {
2037                            SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2038                        });
2039                    });
2040
2041                    options.clear();
2042                    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();
2043
2044                    defaultOption = null;
2045                    for (Option option : theOptions) {
2046                        if (option.getValue().equals(mValue.getContent())) {
2047                            defaultOption = option;
2048                            break;
2049                        }
2050                    }
2051                    if (defaultOption == null && !mValue.getContent().equals("")) {
2052                        // Synthesize default option for custom value
2053                        defaultOption = new Option(mValue.getContent(), mValue.getContent());
2054                    }
2055                    if (defaultOption == null) {
2056                        binding.defaultButton.setVisibility(View.GONE);
2057                    } else {
2058                        theOptions.remove(defaultOption);
2059                        binding.defaultButton.setVisibility(View.VISIBLE);
2060
2061                        final SVG defaultIcon = defaultOption.getIcon();
2062                        if (defaultIcon != null) {
2063                             defaultIcon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
2064                             DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
2065                             Bitmap bitmap = Bitmap.createBitmap((int)(display.heightPixels*display.density/4), (int)(display.heightPixels*display.density/4), Bitmap.Config.ARGB_8888);
2066                             bitmap.setDensity(display.densityDpi);
2067                             Canvas bmcanvas = new Canvas(bitmap);
2068                             defaultIcon.renderToCanvas(bmcanvas);
2069                             binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
2070                        }
2071
2072                        binding.defaultButton.setText(defaultOption.toString());
2073                        binding.defaultButton.setOnClickListener((view) -> {
2074                            loading = true;
2075                            mValue.setContent(defaultOption.getValue());
2076                            execute();
2077                        });
2078                    }
2079
2080                    options.addAll(theOptions);
2081                    binding.buttons.setAdapter(options);
2082                }
2083            }
2084
2085            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
2086                public TextFieldViewHolder(CommandTextFieldBinding binding) {
2087                    super(binding);
2088                    binding.textinput.addTextChangedListener(this);
2089                }
2090                protected Element mValue = null;
2091
2092                @Override
2093                public void bind(Item item) {
2094                    Field field = (Field) item;
2095                    binding.textinputLayout.setHint(field.getLabel().or(""));
2096
2097                    binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
2098                    for (String desc : field.getDesc().asSet()) {
2099                        binding.textinputLayout.setHelperText(desc);
2100                    }
2101
2102                    binding.textinputLayout.setErrorEnabled(field.error != null);
2103                    if (field.error != null) binding.textinputLayout.setError(field.error);
2104
2105                    binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
2106                    String suffixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/suffix-label");
2107                    if (suffixLabel != null) {
2108                        binding.textinputLayout.setSuffixText(suffixLabel);
2109                        binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END);
2110                    }
2111
2112                    String prefixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/prefix-label");
2113                    if (prefixLabel != null) {
2114                        binding.textinputLayout.setPrefixText(prefixLabel);
2115                    }
2116
2117                    mValue = field.getValue();
2118                    binding.textinput.setText(mValue.getContent());
2119                    setupInputType(field.el, binding.textinput, binding.textinputLayout);
2120                }
2121
2122                @Override
2123                public void afterTextChanged(Editable s) {
2124                    if (mValue == null) return;
2125
2126                    mValue.setContent(s.toString());
2127                }
2128
2129                @Override
2130                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
2131
2132                @Override
2133                public void onTextChanged(CharSequence s, int start, int count, int after) { }
2134            }
2135
2136            class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
2137                public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
2138                protected String boundUrl = "";
2139
2140                @Override
2141                public void bind(Item oob) {
2142                    setTextOrHide(binding.desc, Optional.fromNullable(oob.el.findChildContent("desc", "jabber:x:oob")));
2143                    binding.webview.getSettings().setJavaScriptEnabled(true);
2144                    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");
2145                    binding.webview.getSettings().setDatabaseEnabled(true);
2146                    binding.webview.getSettings().setDomStorageEnabled(true);
2147                    binding.webview.setWebChromeClient(new WebChromeClient() {
2148                        @Override
2149                        public void onProgressChanged(WebView view, int newProgress) {
2150                            binding.progressbar.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
2151                            binding.progressbar.setProgress(newProgress);
2152                        }
2153                    });
2154                    binding.webview.setWebViewClient(new WebViewClient() {
2155                        @Override
2156                        public void onPageFinished(WebView view, String url) {
2157                            super.onPageFinished(view, url);
2158                            mTitle = view.getTitle();
2159                            ConversationPagerAdapter.this.notifyDataSetChanged();
2160                        }
2161                    });
2162                    final String url = oob.el.findChildContent("url", "jabber:x:oob");
2163                    if (!boundUrl.equals(url)) {
2164                        binding.webview.addJavascriptInterface(new JsObject(), "xmpp_xep0050");
2165                        binding.webview.loadUrl(url);
2166                        boundUrl = url;
2167                    }
2168                }
2169
2170                class JsObject {
2171                    @JavascriptInterface
2172                    public void execute() { execute("execute"); }
2173
2174                    @JavascriptInterface
2175                    public void execute(String action) {
2176                        getView().post(() -> {
2177                            actionToWebview = null;
2178                            if(CommandSession.this.execute(action)) {
2179                                removeSession(CommandSession.this);
2180                            }
2181                        });
2182                    }
2183
2184                    @JavascriptInterface
2185                    public void preventDefault() {
2186                        actionToWebview = binding.webview;
2187                    }
2188                }
2189            }
2190
2191            class ProgressBarViewHolder extends ViewHolder<CommandProgressBarBinding> {
2192                public ProgressBarViewHolder(CommandProgressBarBinding binding) { super(binding); }
2193
2194                @Override
2195                public void bind(Item item) { }
2196            }
2197
2198            class Item {
2199                protected Element el;
2200                protected int viewType;
2201                protected String error = null;
2202
2203                Item(Element el, int viewType) {
2204                    this.el = el;
2205                    this.viewType = viewType;
2206                }
2207
2208                public boolean validate() {
2209                    error = null;
2210                    return true;
2211                }
2212            }
2213
2214            class Field extends Item {
2215                Field(eu.siacs.conversations.xmpp.forms.Field el, int viewType) { super(el, viewType); }
2216
2217                @Override
2218                public boolean validate() {
2219                    if (!super.validate()) return false;
2220                    if (el.findChild("required", "jabber:x:data") == null) return true;
2221                    if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
2222
2223                    error = "this value is required";
2224                    return false;
2225                }
2226
2227                public String getVar() {
2228                    return el.getAttribute("var");
2229                }
2230
2231                public Optional<String> getType() {
2232                    return Optional.fromNullable(el.getAttribute("type"));
2233                }
2234
2235                public Optional<String> getLabel() {
2236                    String label = el.getAttribute("label");
2237                    if (label == null) label = getVar();
2238                    return Optional.fromNullable(label);
2239                }
2240
2241                public Optional<String> getDesc() {
2242                    return Optional.fromNullable(el.findChildContent("desc", "jabber:x:data"));
2243                }
2244
2245                public Element getValue() {
2246                    Element value = el.findChild("value", "jabber:x:data");
2247                    if (value == null) {
2248                        value = el.addChild("value", "jabber:x:data");
2249                    }
2250                    return value;
2251                }
2252
2253                public List<Option> getOptions() {
2254                    return Option.forField(el);
2255                }
2256            }
2257
2258            class Cell extends Item {
2259                protected Field reported;
2260
2261                Cell(Field reported, Element item) {
2262                    super(item, TYPE_RESULT_CELL);
2263                    this.reported = reported;
2264                }
2265            }
2266
2267            protected Field mkField(Element el) {
2268                int viewType = -1;
2269
2270                String formType = responseElement.getAttribute("type");
2271                if (formType != null) {
2272                    String fieldType = el.getAttribute("type");
2273                    if (fieldType == null) fieldType = "text-single";
2274
2275                    if (formType.equals("result") || fieldType.equals("fixed")) {
2276                        viewType = TYPE_RESULT_FIELD;
2277                    } else if (formType.equals("form")) {
2278                        if (fieldType.equals("boolean")) {
2279                            if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2280                                viewType = TYPE_BUTTON_GRID_FIELD;
2281                            } else {
2282                                viewType = TYPE_CHECKBOX_FIELD;
2283                            }
2284                        } else if (fieldType.equals("list-single")) {
2285                            Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2286                            if (Option.forField(el).size() > 9) {
2287                                viewType = TYPE_SEARCH_LIST_FIELD;
2288                            } else if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2289                                viewType = TYPE_BUTTON_GRID_FIELD;
2290                            } else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
2291                                viewType = TYPE_RADIO_EDIT_FIELD;
2292                            } else {
2293                                viewType = TYPE_SPINNER_FIELD;
2294                            }
2295                        } else {
2296                            viewType = TYPE_TEXT_FIELD;
2297                        }
2298                    }
2299
2300                    return new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), viewType);
2301                }
2302
2303                return null;
2304            }
2305
2306            protected Item mkItem(Element el, int pos) {
2307                int viewType = -1;
2308
2309                if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
2310                    if (el.getName().equals("note")) {
2311                        viewType = TYPE_NOTE;
2312                    } else if (el.getNamespace().equals("jabber:x:oob")) {
2313                        viewType = TYPE_WEB;
2314                    } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
2315                        viewType = TYPE_NOTE;
2316                    } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
2317                        Field field = mkField(el);
2318                        if (field != null) {
2319                            items.put(pos, field);
2320                            return field;
2321                        }
2322                    }
2323                } else if (response != null) {
2324                    viewType = TYPE_ERROR;
2325                }
2326
2327                Item item = new Item(el, viewType);
2328                items.put(pos, item);
2329                return item;
2330            }
2331
2332            class ActionsAdapter extends ArrayAdapter<Pair<String, String>> {
2333                protected Context ctx;
2334
2335                public ActionsAdapter(Context ctx) {
2336                    super(ctx, R.layout.simple_list_item);
2337                    this.ctx = ctx;
2338                }
2339
2340                @Override
2341                public View getView(int position, View convertView, ViewGroup parent) {
2342                    View v = super.getView(position, convertView, parent);
2343                    TextView tv = (TextView) v.findViewById(android.R.id.text1);
2344                    tv.setGravity(Gravity.CENTER);
2345                    tv.setText(getItem(position).second);
2346                    int resId = ctx.getResources().getIdentifier("action_" + getItem(position).first, "string" , ctx.getPackageName());
2347                    if (resId != 0 && getItem(position).second.equals(getItem(position).first)) tv.setText(ctx.getResources().getString(resId));
2348                    tv.setTextColor(ContextCompat.getColor(ctx, R.color.white));
2349                    tv.setBackgroundColor(UIHelper.getColorForName(getItem(position).first));
2350                    return v;
2351                }
2352
2353                public int getPosition(String s) {
2354                    for(int i = 0; i < getCount(); i++) {
2355                        if (getItem(i).first.equals(s)) return i;
2356                    }
2357                    return -1;
2358                }
2359
2360                public int countExceptCancel() {
2361                    int count = 0;
2362                    for(int i = 0; i < getCount(); i++) {
2363                        if (!getItem(i).first.equals("cancel")) count++;
2364                    }
2365                    return count;
2366                }
2367
2368                public void clearExceptCancel() {
2369                    Pair<String,String> cancelItem = null;
2370                    for(int i = 0; i < getCount(); i++) {
2371                        if (getItem(i).first.equals("cancel")) cancelItem = getItem(i);
2372                    }
2373                    clear();
2374                    if (cancelItem != null) add(cancelItem);
2375                }
2376            }
2377
2378            final int TYPE_ERROR = 1;
2379            final int TYPE_NOTE = 2;
2380            final int TYPE_WEB = 3;
2381            final int TYPE_RESULT_FIELD = 4;
2382            final int TYPE_TEXT_FIELD = 5;
2383            final int TYPE_CHECKBOX_FIELD = 6;
2384            final int TYPE_SPINNER_FIELD = 7;
2385            final int TYPE_RADIO_EDIT_FIELD = 8;
2386            final int TYPE_RESULT_CELL = 9;
2387            final int TYPE_PROGRESSBAR = 10;
2388            final int TYPE_SEARCH_LIST_FIELD = 11;
2389            final int TYPE_ITEM_CARD = 12;
2390            final int TYPE_BUTTON_GRID_FIELD = 13;
2391
2392            protected boolean loading = false;
2393            protected Timer loadingTimer = new Timer();
2394            protected String mTitle;
2395            protected String mNode;
2396            protected CommandPageBinding mBinding = null;
2397            protected IqPacket response = null;
2398            protected Element responseElement = null;
2399            protected List<Field> reported = null;
2400            protected SparseArray<Item> items = new SparseArray<>();
2401            protected XmppConnectionService xmppConnectionService;
2402            protected ActionsAdapter actionsAdapter;
2403            protected GridLayoutManager layoutManager;
2404            protected WebView actionToWebview = null;
2405            protected int fillableFieldCount = 0;
2406            protected IqPacket pendingResponsePacket = null;
2407
2408            CommandSession(String title, String node, XmppConnectionService xmppConnectionService) {
2409                loading();
2410                mTitle = title;
2411                mNode = node;
2412                this.xmppConnectionService = xmppConnectionService;
2413                if (mPager != null) setupLayoutManager();
2414                actionsAdapter = new ActionsAdapter(xmppConnectionService);
2415                actionsAdapter.registerDataSetObserver(new DataSetObserver() {
2416                    @Override
2417                    public void onChanged() {
2418                        if (mBinding == null) return;
2419
2420                        mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
2421                    }
2422
2423                    @Override
2424                    public void onInvalidated() {}
2425                });
2426            }
2427
2428            public String getTitle() {
2429                return mTitle;
2430            }
2431
2432            public void updateWithResponse(final IqPacket iq) {
2433                if (getView() != null && getView().isAttachedToWindow()) {
2434                    getView().post(() -> updateWithResponseUiThread(iq));
2435                } else {
2436                    pendingResponsePacket = iq;
2437                }
2438            }
2439
2440            protected void updateWithResponseUiThread(final IqPacket iq) {
2441                this.loadingTimer.cancel();
2442                this.loadingTimer = new Timer();
2443                this.loading = false;
2444                this.responseElement = null;
2445                this.fillableFieldCount = 0;
2446                this.reported = null;
2447                this.response = iq;
2448                this.items.clear();
2449                this.actionsAdapter.clear();
2450                layoutManager.setSpanCount(1);
2451
2452                boolean actionsCleared = false;
2453                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
2454                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
2455                    if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) {
2456                        xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true);
2457                    }
2458
2459                    for (Element el : command.getChildren()) {
2460                        if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
2461                            for (Element action : el.getChildren()) {
2462                                if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
2463                                if (action.getName().equals("execute")) continue;
2464
2465                                actionsAdapter.add(Pair.create(action.getName(), action.getName()));
2466                            }
2467                        }
2468                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
2469                            Data form = Data.parse(el);
2470                            String title = form.getTitle();
2471                            if (title != null) {
2472                                mTitle = title;
2473                                ConversationPagerAdapter.this.notifyDataSetChanged();
2474                            }
2475
2476                            if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
2477                                this.responseElement = el;
2478                                setupReported(el.findChild("reported", "jabber:x:data"));
2479                                if (mBinding != null) mBinding.form.setLayoutManager(setupLayoutManager());
2480                            }
2481
2482                            eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
2483                            if (actionList != null) {
2484                                actionsAdapter.clear();
2485
2486                                for (Option action : actionList.getOptions()) {
2487                                    actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
2488                                }
2489                            }
2490
2491                            String fillableFieldType = null;
2492                            String fillableFieldValue = null;
2493                            for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
2494                                if ((field.getType() == null || (!field.getType().equals("hidden") && !field.getType().equals("fixed"))) && field.getFieldName() != null && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
2495                                    fillableFieldType = field.getType();
2496                                    fillableFieldValue = field.getValue();
2497                                    fillableFieldCount++;
2498                                }
2499                            }
2500
2501                            if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 2 && fillableFieldType != null && (fillableFieldType.equals("list-single") || (fillableFieldType.equals("boolean") && fillableFieldValue == null))) {
2502                                actionsCleared = true;
2503                                actionsAdapter.clearExceptCancel();
2504                            }
2505                            break;
2506                        }
2507                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
2508                            String url = el.findChildContent("url", "jabber:x:oob");
2509                            if (url != null) {
2510                                String scheme = Uri.parse(url).getScheme();
2511                                if (scheme.equals("http") || scheme.equals("https")) {
2512                                    this.responseElement = el;
2513                                    break;
2514                                }
2515                                if (scheme.equals("xmpp")) {
2516                                    final Intent intent = new Intent(getView().getContext(), UriHandlerActivity.class);
2517                                    intent.setAction(Intent.ACTION_VIEW);
2518                                    intent.setData(Uri.parse(url));
2519                                    getView().getContext().startActivity(intent);
2520                                    break;
2521                                }
2522                            }
2523                        }
2524                        if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
2525                            this.responseElement = el;
2526                            break;
2527                        }
2528                    }
2529
2530                    if (responseElement == null && command.getAttribute("status") != null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
2531                        if (mNode.equals("jabber:iq:register") && command.getAttribute("status").equals("canceled")) {
2532                            if (xmppConnectionService.isOnboarding()) {
2533                                if (!xmppConnectionService.getPreferences().contains("onboarding_action")) {
2534                                    xmppConnectionService.getPreferences().edit().putString("onboarding_action", "cancel").commit();
2535                                }
2536                                xmppConnectionService.deleteAccount(getAccount());
2537                            }
2538                            xmppConnectionService.archiveConversation(Conversation.this);
2539                        }
2540
2541                        removeSession(this);
2542                        return;
2543                    }
2544
2545                    if (command.getAttribute("status").equals("executing") && actionsAdapter.countExceptCancel() < 1 && !actionsCleared) {
2546                        // No actions have been given, but we are not done?
2547                        // This is probably a spec violation, but we should do *something*
2548                        actionsAdapter.add(Pair.create("execute", "execute"));
2549                    }
2550
2551                    if (!actionsAdapter.isEmpty() || fillableFieldCount > 0) {
2552                        if (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled")) {
2553                            actionsAdapter.add(Pair.create("close", "close"));
2554                        } else if (actionsAdapter.getPosition("cancel") < 0 && !xmppConnectionService.isOnboarding()) {
2555                            actionsAdapter.insert(Pair.create("cancel", "cancel"), 0);
2556                        }
2557                    }
2558                }
2559
2560                if (actionsAdapter.isEmpty()) {
2561                    actionsAdapter.add(Pair.create("close", "close"));
2562                }
2563
2564                actionsAdapter.sort((x, y) -> {
2565                    if (x.first.equals("cancel")) return -1;
2566                    if (y.first.equals("cancel")) return 1;
2567                    if (x.first.equals("prev") && xmppConnectionService.isOnboarding()) return -1;
2568                    if (y.first.equals("prev") && xmppConnectionService.isOnboarding()) return 1;
2569                    return 0;
2570                });
2571
2572                Data dataForm = null;
2573                if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
2574                if (mNode.equals("jabber:iq:register") &&
2575                    xmppConnectionService.getPreferences().contains("onboarding_action") &&
2576                    dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
2577
2578
2579                    dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
2580                    execute();
2581                }
2582                xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
2583                notifyDataSetChanged();
2584            }
2585
2586            protected void setupReported(Element el) {
2587                if (el == null) {
2588                    reported = null;
2589                    return;
2590                }
2591
2592                reported = new ArrayList<>();
2593                for (Element fieldEl : el.getChildren()) {
2594                    if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
2595                    reported.add(mkField(fieldEl));
2596                }
2597            }
2598
2599            @Override
2600            public int getItemCount() {
2601                if (loading) return 1;
2602                if (response == null) return 0;
2603                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
2604                    int i = 0;
2605                    for (Element el : responseElement.getChildren()) {
2606                        if (!el.getNamespace().equals("jabber:x:data")) continue;
2607                        if (el.getName().equals("title")) continue;
2608                        if (el.getName().equals("field")) {
2609                            String type = el.getAttribute("type");
2610                            if (type != null && type.equals("hidden")) continue;
2611                            if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2612                        }
2613
2614                        if (el.getName().equals("reported") || el.getName().equals("item")) {
2615                            if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2616                                if (el.getName().equals("reported")) continue;
2617                                i += 1;
2618                            } else {
2619                                if (reported != null) i += reported.size();
2620                            }
2621                            continue;
2622                        }
2623
2624                        i++;
2625                    }
2626                    return i;
2627                }
2628                return 1;
2629            }
2630
2631            public Item getItem(int position) {
2632                if (loading) return new Item(null, TYPE_PROGRESSBAR);
2633                if (items.get(position) != null) return items.get(position);
2634                if (response == null) return null;
2635
2636                if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
2637                    if (responseElement.getNamespace().equals("jabber:x:data")) {
2638                        int i = 0;
2639                        for (Element el : responseElement.getChildren()) {
2640                            if (!el.getNamespace().equals("jabber:x:data")) continue;
2641                            if (el.getName().equals("title")) continue;
2642                            if (el.getName().equals("field")) {
2643                                String type = el.getAttribute("type");
2644                                if (type != null && type.equals("hidden")) continue;
2645                                if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2646                            }
2647
2648                            if (el.getName().equals("reported") || el.getName().equals("item")) {
2649                                Cell cell = null;
2650
2651                                if (reported != null) {
2652                                    if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2653                                        if (el.getName().equals("reported")) continue;
2654                                        if (i == position) {
2655                                            items.put(position, new Item(el, TYPE_ITEM_CARD));
2656                                            return items.get(position);
2657                                        }
2658                                    } else {
2659                                        if (reported.size() > position - i) {
2660                                            Field reportedField = reported.get(position - i);
2661                                            Element itemField = null;
2662                                            if (el.getName().equals("item")) {
2663                                                for (Element subel : el.getChildren()) {
2664                                                    if (subel.getAttribute("var").equals(reportedField.getVar())) {
2665                                                       itemField = subel;
2666                                                       break;
2667                                                    }
2668                                                }
2669                                            }
2670                                            cell = new Cell(reportedField, itemField);
2671                                        } else {
2672                                            i += reported.size();
2673                                            continue;
2674                                        }
2675                                    }
2676                                }
2677
2678                                if (cell != null) {
2679                                    items.put(position, cell);
2680                                    return cell;
2681                                }
2682                            }
2683
2684                            if (i < position) {
2685                                i++;
2686                                continue;
2687                            }
2688
2689                            return mkItem(el, position);
2690                        }
2691                    }
2692                }
2693
2694                return mkItem(responseElement == null ? response : responseElement, position);
2695            }
2696
2697            @Override
2698            public int getItemViewType(int position) {
2699                return getItem(position).viewType;
2700            }
2701
2702            @Override
2703            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2704                switch(viewType) {
2705                    case TYPE_ERROR: {
2706                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2707                        return new ErrorViewHolder(binding);
2708                    }
2709                    case TYPE_NOTE: {
2710                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2711                        return new NoteViewHolder(binding);
2712                    }
2713                    case TYPE_WEB: {
2714                        CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2715                        return new WebViewHolder(binding);
2716                    }
2717                    case TYPE_RESULT_FIELD: {
2718                        CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2719                        return new ResultFieldViewHolder(binding);
2720                    }
2721                    case TYPE_RESULT_CELL: {
2722                        CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2723                        return new ResultCellViewHolder(binding);
2724                    }
2725                    case TYPE_ITEM_CARD: {
2726                        CommandItemCardBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_item_card, container, false);
2727                        return new ItemCardViewHolder(binding);
2728                    }
2729                    case TYPE_CHECKBOX_FIELD: {
2730                        CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2731                        return new CheckboxFieldViewHolder(binding);
2732                    }
2733                    case TYPE_SEARCH_LIST_FIELD: {
2734                        CommandSearchListFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_search_list_field, container, false);
2735                        return new SearchListFieldViewHolder(binding);
2736                    }
2737                    case TYPE_RADIO_EDIT_FIELD: {
2738                        CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2739                        return new RadioEditFieldViewHolder(binding);
2740                    }
2741                    case TYPE_SPINNER_FIELD: {
2742                        CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2743                        return new SpinnerFieldViewHolder(binding);
2744                    }
2745                    case TYPE_BUTTON_GRID_FIELD: {
2746                        CommandButtonGridFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_button_grid_field, container, false);
2747                        return new ButtonGridFieldViewHolder(binding);
2748                    }
2749                    case TYPE_TEXT_FIELD: {
2750                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2751                        return new TextFieldViewHolder(binding);
2752                    }
2753                    case TYPE_PROGRESSBAR: {
2754                        CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
2755                        return new ProgressBarViewHolder(binding);
2756                    }
2757                    default:
2758                        throw new IllegalArgumentException("Unknown viewType: " + viewType);
2759                }
2760            }
2761
2762            @Override
2763            public void onBindViewHolder(ViewHolder viewHolder, int position) {
2764                viewHolder.bind(getItem(position));
2765            }
2766
2767            public View getView() {
2768                if (mBinding == null) return null;
2769                return mBinding.getRoot();
2770            }
2771
2772            public boolean validate() {
2773                int count = getItemCount();
2774                boolean isValid = true;
2775                for (int i = 0; i < count; i++) {
2776                    boolean oneIsValid = getItem(i).validate();
2777                    isValid = isValid && oneIsValid;
2778                }
2779                notifyDataSetChanged();
2780                return isValid;
2781            }
2782
2783            public boolean execute() {
2784                return execute("execute");
2785            }
2786
2787            public boolean execute(int actionPosition) {
2788                return execute(actionsAdapter.getItem(actionPosition).first);
2789            }
2790
2791            public boolean execute(String action) {
2792                if (!action.equals("cancel") && !action.equals("prev") && !validate()) return false;
2793
2794                if (response == null) return true;
2795                Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2796                if (command == null) return true;
2797                String status = command.getAttribute("status");
2798                if (status == null || (!status.equals("executing") && !action.equals("prev"))) return true;
2799
2800                if (actionToWebview != null && !action.equals("cancel")) {
2801                    actionToWebview.postWebMessage(new WebMessage("xmpp_xep0050/" + action), Uri.parse("*"));
2802                    return false;
2803                }
2804
2805                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2806                packet.setTo(response.getFrom());
2807                final Element c = packet.addChild("command", Namespace.COMMANDS);
2808                c.setAttribute("node", mNode);
2809                c.setAttribute("sessionid", command.getAttribute("sessionid"));
2810
2811                String formType = responseElement == null ? null : responseElement.getAttribute("type");
2812                if (!action.equals("cancel") &&
2813                    !action.equals("prev") &&
2814                    responseElement != null &&
2815                    responseElement.getName().equals("x") &&
2816                    responseElement.getNamespace().equals("jabber:x:data") &&
2817                    formType != null && formType.equals("form")) {
2818
2819                    Data form = Data.parse(responseElement);
2820                    eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
2821                    if (actionList != null) {
2822                        actionList.setValue(action);
2823                        c.setAttribute("action", "execute");
2824                    }
2825
2826                    if (mNode.equals("jabber:iq:register") && xmppConnectionService.isOnboarding() && form.getValue("gateway-jid") != null) {
2827                        xmppConnectionService.getPreferences().edit().putString("onboarding_action", form.getValue("gateway-jid")).commit();
2828                    }
2829
2830                    responseElement.setAttribute("type", "submit");
2831                    Element rsm = responseElement.findChild("set", "http://jabber.org/protocol/rsm");
2832                    if (rsm != null) {
2833                        Element max = new Element("max", "http://jabber.org/protocol/rsm");
2834                        max.setContent("1000");
2835                        rsm.addChild(max);
2836                    }
2837
2838                    c.addChild(responseElement);
2839                }
2840
2841                if (c.getAttribute("action") == null) c.setAttribute("action", action);
2842
2843                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2844                    updateWithResponse(iq);
2845                });
2846
2847                loading();
2848                return false;
2849            }
2850
2851            protected void loading() {
2852                View v = getView();
2853                loadingTimer.schedule(new TimerTask() {
2854                    @Override
2855                    public void run() {
2856                        View v2 = getView();
2857                        loading = true;
2858
2859                        if (v == null && v2 == null) return;
2860                        (v == null ? v2 : v).post(() -> notifyDataSetChanged());
2861                    }
2862                }, 500);
2863            }
2864
2865            protected GridLayoutManager setupLayoutManager() {
2866                int spanCount = 1;
2867
2868                if (reported != null && mPager != null) {
2869                    float screenWidth = mPager.getContext().getResources().getDisplayMetrics().widthPixels;
2870                    TextPaint paint = ((TextView) LayoutInflater.from(mPager.getContext()).inflate(R.layout.command_result_cell, null)).getPaint();
2871                    float tableHeaderWidth = reported.stream().reduce(
2872                        0f,
2873                        (total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------"), paint),
2874                        (a, b) -> a + b
2875                    );
2876
2877                    spanCount = tableHeaderWidth > 0.65 * screenWidth ? 1 : this.reported.size();
2878                }
2879
2880                if (layoutManager != null && layoutManager.getSpanCount() != spanCount) {
2881                    items.clear();
2882                    notifyDataSetChanged();
2883                }
2884
2885                layoutManager = new GridLayoutManager(mPager.getContext(), spanCount);
2886                layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2887                    @Override
2888                    public int getSpanSize(int position) {
2889                        if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2890                        return 1;
2891                    }
2892                });
2893                return layoutManager;
2894            }
2895
2896            public void setBinding(CommandPageBinding b) {
2897                mBinding = b;
2898                // https://stackoverflow.com/a/32350474/8611
2899                mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
2900                    @Override
2901                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
2902                        if(rv.getChildCount() > 0) {
2903                            int[] location = new int[2];
2904                            rv.getLocationOnScreen(location);
2905                            View childView = rv.findChildViewUnder(e.getX(), e.getY());
2906                            if (childView instanceof ViewGroup) {
2907                                childView = findViewAt((ViewGroup) childView, location[0] + e.getX(), location[1] + e.getY());
2908                            }
2909                            int action = e.getAction();
2910                            switch (action) {
2911                                case MotionEvent.ACTION_DOWN:
2912                                    if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(1)) || childView instanceof WebView) {
2913                                        rv.requestDisallowInterceptTouchEvent(true);
2914                                    }
2915                                case MotionEvent.ACTION_UP:
2916                                    if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(-11)) || childView instanceof WebView) {
2917                                        rv.requestDisallowInterceptTouchEvent(true);
2918                                    }
2919                            }
2920                        }
2921
2922                        return false;
2923                    }
2924
2925                    @Override
2926                    public void onRequestDisallowInterceptTouchEvent(boolean disallow) { }
2927
2928                    @Override
2929                    public void onTouchEvent(RecyclerView rv, MotionEvent e) { }
2930                });
2931                mBinding.form.setLayoutManager(setupLayoutManager());
2932                mBinding.form.setAdapter(this);
2933                mBinding.actions.setAdapter(actionsAdapter);
2934                mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2935                    if (execute(pos)) {
2936                        removeSession(CommandSession.this);
2937                    }
2938                });
2939
2940                actionsAdapter.notifyDataSetChanged();
2941
2942                if (pendingResponsePacket != null) {
2943                    final IqPacket pending = pendingResponsePacket;
2944                    pendingResponsePacket = null;
2945                    updateWithResponseUiThread(pending);
2946                }
2947            }
2948
2949            // https://stackoverflow.com/a/36037991/8611
2950            private View findViewAt(ViewGroup viewGroup, float x, float y) {
2951                for(int i = 0; i < viewGroup.getChildCount(); i++) {
2952                    View child = viewGroup.getChildAt(i);
2953                    if (child instanceof ViewGroup && !(child instanceof AbsListView) && !(child instanceof WebView)) {
2954                        View foundView = findViewAt((ViewGroup) child, x, y);
2955                        if (foundView != null && foundView.isShown()) {
2956                            return foundView;
2957                        }
2958                    } else {
2959                        int[] location = new int[2];
2960                        child.getLocationOnScreen(location);
2961                        Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight());
2962                        if (rect.contains((int)x, (int)y)) {
2963                            return child;
2964                        }
2965                    }
2966                }
2967
2968                return null;
2969            }
2970        }
2971    }
2972}