Conversation.java

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