Conversation.java

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