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