Conversation.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.net.Uri;
   6import android.text.Editable;
   7import android.text.InputType;
   8import android.text.TextUtils;
   9import android.text.TextWatcher;
  10import android.view.LayoutInflater;
  11import android.view.View;
  12import android.view.ViewGroup;
  13import android.widget.ArrayAdapter;
  14import android.webkit.WebView;
  15import android.webkit.WebViewClient;
  16
  17import androidx.annotation.NonNull;
  18import androidx.annotation.Nullable;
  19import androidx.databinding.DataBindingUtil;
  20import androidx.databinding.ViewDataBinding;
  21import androidx.viewpager.widget.PagerAdapter;
  22import androidx.recyclerview.widget.RecyclerView;
  23import androidx.recyclerview.widget.LinearLayoutManager;
  24import androidx.viewpager.widget.ViewPager;
  25
  26import com.google.android.material.tabs.TabLayout;
  27import com.google.common.collect.ComparisonChain;
  28import com.google.common.collect.Lists;
  29
  30import org.json.JSONArray;
  31import org.json.JSONException;
  32import org.json.JSONObject;
  33
  34import java.util.ArrayList;
  35import java.util.Collections;
  36import java.util.Iterator;
  37import java.util.List;
  38import java.util.ListIterator;
  39import java.util.concurrent.atomic.AtomicBoolean;
  40
  41import eu.siacs.conversations.Config;
  42import eu.siacs.conversations.R;
  43import eu.siacs.conversations.crypto.OmemoSetting;
  44import eu.siacs.conversations.crypto.PgpDecryptionService;
  45import eu.siacs.conversations.databinding.CommandPageBinding;
  46import eu.siacs.conversations.databinding.CommandNoteBinding;
  47import eu.siacs.conversations.databinding.CommandResultFieldBinding;
  48import eu.siacs.conversations.databinding.CommandTextFieldBinding;
  49import eu.siacs.conversations.databinding.CommandWebviewBinding;
  50import eu.siacs.conversations.persistance.DatabaseBackend;
  51import eu.siacs.conversations.services.AvatarService;
  52import eu.siacs.conversations.services.QuickConversationsService;
  53import eu.siacs.conversations.services.XmppConnectionService;
  54import eu.siacs.conversations.utils.JidHelper;
  55import eu.siacs.conversations.utils.MessageUtils;
  56import eu.siacs.conversations.utils.UIHelper;
  57import eu.siacs.conversations.xml.Element;
  58import eu.siacs.conversations.xml.Namespace;
  59import eu.siacs.conversations.xmpp.Jid;
  60import eu.siacs.conversations.xmpp.chatstate.ChatState;
  61import eu.siacs.conversations.xmpp.mam.MamReference;
  62import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  63
  64import static eu.siacs.conversations.entities.Bookmark.printableValue;
  65
  66
  67public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
  68    public static final String TABLENAME = "conversations";
  69
  70    public static final int STATUS_AVAILABLE = 0;
  71    public static final int STATUS_ARCHIVED = 1;
  72
  73    public static final String NAME = "name";
  74    public static final String ACCOUNT = "accountUuid";
  75    public static final String CONTACT = "contactUuid";
  76    public static final String CONTACTJID = "contactJid";
  77    public static final String STATUS = "status";
  78    public static final String CREATED = "created";
  79    public static final String MODE = "mode";
  80    public static final String ATTRIBUTES = "attributes";
  81
  82    public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
  83    public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
  84    public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
  85    public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
  86    public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
  87    static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
  88    static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
  89    static final String ATTRIBUTE_MODERATED = "moderated";
  90    static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
  91    private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
  92    private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
  93    private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
  94    private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
  95    private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
  96    protected final ArrayList<Message> messages = new ArrayList<>();
  97    public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
  98    protected Account account = null;
  99    private String draftMessage;
 100    private final String name;
 101    private final String contactUuid;
 102    private final String accountUuid;
 103    private Jid contactJid;
 104    private int status;
 105    private final long created;
 106    private int mode;
 107    private JSONObject attributes;
 108    private Jid nextCounterpart;
 109    private transient MucOptions mucOptions = null;
 110    private boolean messagesLeftOnServer = true;
 111    private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
 112    private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
 113    private String mFirstMamReference = null;
 114    protected int mCurrentTab = -1;
 115    protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
 116
 117    public Conversation(final String name, final Account account, final Jid contactJid,
 118                        final int mode) {
 119        this(java.util.UUID.randomUUID().toString(), name, null, account
 120                        .getUuid(), contactJid, System.currentTimeMillis(),
 121                STATUS_AVAILABLE, mode, "");
 122        this.account = account;
 123    }
 124
 125    public Conversation(final String uuid, final String name, final String contactUuid,
 126                        final String accountUuid, final Jid contactJid, final long created, final int status,
 127                        final int mode, final String attributes) {
 128        this.uuid = uuid;
 129        this.name = name;
 130        this.contactUuid = contactUuid;
 131        this.accountUuid = accountUuid;
 132        this.contactJid = contactJid;
 133        this.created = created;
 134        this.status = status;
 135        this.mode = mode;
 136        try {
 137            this.attributes = new JSONObject(attributes == null ? "" : attributes);
 138        } catch (JSONException e) {
 139            this.attributes = new JSONObject();
 140        }
 141    }
 142
 143    public static Conversation fromCursor(Cursor cursor) {
 144        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 145                cursor.getString(cursor.getColumnIndex(NAME)),
 146                cursor.getString(cursor.getColumnIndex(CONTACT)),
 147                cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 148                JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
 149                cursor.getLong(cursor.getColumnIndex(CREATED)),
 150                cursor.getInt(cursor.getColumnIndex(STATUS)),
 151                cursor.getInt(cursor.getColumnIndex(MODE)),
 152                cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 153    }
 154
 155    public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
 156        for (int i = messages.size() - 1; i >= 0; --i) {
 157            final Message message = messages.get(i);
 158            if (message.getStatus() <= Message.STATUS_RECEIVED
 159                    && (message.markable || isPrivateAndNonAnonymousMuc)
 160                    && !message.isPrivateMessage()) {
 161                return message;
 162            }
 163        }
 164        return null;
 165    }
 166
 167    private static boolean suitableForOmemoByDefault(final Conversation conversation) {
 168        if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
 169            return false;
 170        }
 171        if (conversation.getContact().isOwnServer()) {
 172            return false;
 173        }
 174        final String contact = conversation.getJid().getDomain().toEscapedString();
 175        final String account = conversation.getAccount().getServer();
 176        if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
 177            return false;
 178        }
 179        return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
 180    }
 181
 182    public boolean hasMessagesLeftOnServer() {
 183        return messagesLeftOnServer;
 184    }
 185
 186    public void setHasMessagesLeftOnServer(boolean value) {
 187        this.messagesLeftOnServer = value;
 188    }
 189
 190    public Message getFirstUnreadMessage() {
 191        Message first = null;
 192        synchronized (this.messages) {
 193            for (int i = messages.size() - 1; i >= 0; --i) {
 194                if (messages.get(i).isRead()) {
 195                    return first;
 196                } else {
 197                    first = messages.get(i);
 198                }
 199            }
 200        }
 201        return first;
 202    }
 203
 204    public String findMostRecentRemoteDisplayableId() {
 205        final boolean multi = mode == Conversation.MODE_MULTI;
 206        synchronized (this.messages) {
 207            for (final Message message : Lists.reverse(this.messages)) {
 208                if (message.getStatus() == Message.STATUS_RECEIVED) {
 209                    final String serverMsgId = message.getServerMsgId();
 210                    if (serverMsgId != null && multi) {
 211                        return serverMsgId;
 212                    }
 213                    return message.getRemoteMsgId();
 214                }
 215            }
 216        }
 217        return null;
 218    }
 219
 220    public int countFailedDeliveries() {
 221        int count = 0;
 222        synchronized (this.messages) {
 223            for(final Message message : this.messages) {
 224                if (message.getStatus() == Message.STATUS_SEND_FAILED) {
 225                    ++count;
 226                }
 227            }
 228        }
 229        return count;
 230    }
 231
 232    public Message getLastEditableMessage() {
 233        synchronized (this.messages) {
 234            for (final Message message : Lists.reverse(this.messages)) {
 235                if (message.isEditable()) {
 236                    if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
 237                        return null;
 238                    }
 239                    return message;
 240                }
 241            }
 242        }
 243        return null;
 244    }
 245
 246
 247    public Message findUnsentMessageWithUuid(String uuid) {
 248        synchronized (this.messages) {
 249            for (final Message message : this.messages) {
 250                final int s = message.getStatus();
 251                if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 252                    return message;
 253                }
 254            }
 255        }
 256        return null;
 257    }
 258
 259    public void findWaitingMessages(OnMessageFound onMessageFound) {
 260        final ArrayList<Message> results = new ArrayList<>();
 261        synchronized (this.messages) {
 262            for (Message message : this.messages) {
 263                if (message.getStatus() == Message.STATUS_WAITING) {
 264                    results.add(message);
 265                }
 266            }
 267        }
 268        for (Message result : results) {
 269            onMessageFound.onMessageFound(result);
 270        }
 271    }
 272
 273    public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) {
 274        final ArrayList<Message> results = new ArrayList<>();
 275        synchronized (this.messages) {
 276            for (final Message message : this.messages) {
 277                if (message.isRead()) {
 278                    continue;
 279                }
 280                results.add(message);
 281            }
 282        }
 283        for (final Message result : results) {
 284            onMessageFound.onMessageFound(result);
 285        }
 286    }
 287
 288    public Message findMessageWithFileAndUuid(final String uuid) {
 289        synchronized (this.messages) {
 290            for (final Message message : this.messages) {
 291                final Transferable transferable = message.getTransferable();
 292                final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
 293                if (message.getUuid().equals(uuid)
 294                        && message.getEncryption() != Message.ENCRYPTION_PGP
 295                        && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
 296                    return message;
 297                }
 298            }
 299        }
 300        return null;
 301    }
 302
 303    public Message findMessageWithUuid(final String uuid) {
 304        synchronized (this.messages) {
 305            for (final Message message : this.messages) {
 306                if (message.getUuid().equals(uuid)) {
 307                    return message;
 308                }
 309            }
 310        }
 311        return null;
 312    }
 313
 314    public boolean markAsDeleted(final List<String> uuids) {
 315        boolean deleted = false;
 316        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 317        synchronized (this.messages) {
 318            for (Message message : this.messages) {
 319                if (uuids.contains(message.getUuid())) {
 320                    message.setDeleted(true);
 321                    deleted = true;
 322                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 323                        pgpDecryptionService.discard(message);
 324                    }
 325                }
 326            }
 327        }
 328        return deleted;
 329    }
 330
 331    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
 332        boolean changed = false;
 333        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 334        synchronized (this.messages) {
 335            for (Message message : this.messages) {
 336                for (final DatabaseBackend.FilePathInfo file : files)
 337                    if (file.uuid.toString().equals(message.getUuid())) {
 338                        message.setDeleted(file.deleted);
 339                        changed = true;
 340                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
 341                            pgpDecryptionService.discard(message);
 342                        }
 343                    }
 344            }
 345        }
 346        return changed;
 347    }
 348
 349    public void clearMessages() {
 350        synchronized (this.messages) {
 351            this.messages.clear();
 352        }
 353    }
 354
 355    public boolean setIncomingChatState(ChatState state) {
 356        if (this.mIncomingChatState == state) {
 357            return false;
 358        }
 359        this.mIncomingChatState = state;
 360        return true;
 361    }
 362
 363    public ChatState getIncomingChatState() {
 364        return this.mIncomingChatState;
 365    }
 366
 367    public boolean setOutgoingChatState(ChatState state) {
 368        if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
 369            if (this.mOutgoingChatState != state) {
 370                this.mOutgoingChatState = state;
 371                return true;
 372            }
 373        }
 374        return false;
 375    }
 376
 377    public ChatState getOutgoingChatState() {
 378        return this.mOutgoingChatState;
 379    }
 380
 381    public void trim() {
 382        synchronized (this.messages) {
 383            final int size = messages.size();
 384            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 385            if (size > maxsize) {
 386                List<Message> discards = this.messages.subList(0, size - maxsize);
 387                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 388                if (pgpDecryptionService != null) {
 389                    pgpDecryptionService.discard(discards);
 390                }
 391                discards.clear();
 392                untieMessages();
 393            }
 394        }
 395    }
 396
 397    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 398        final ArrayList<Message> results = new ArrayList<>();
 399        synchronized (this.messages) {
 400            for (Message message : this.messages) {
 401                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
 402                    results.add(message);
 403                }
 404            }
 405        }
 406        for (Message result : results) {
 407            onMessageFound.onMessageFound(result);
 408        }
 409    }
 410
 411    public Message findSentMessageWithUuidOrRemoteId(String id) {
 412        synchronized (this.messages) {
 413            for (Message message : this.messages) {
 414                if (id.equals(message.getUuid())
 415                        || (message.getStatus() >= Message.STATUS_SEND
 416                        && id.equals(message.getRemoteMsgId()))) {
 417                    return message;
 418                }
 419            }
 420        }
 421        return null;
 422    }
 423
 424    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
 425        synchronized (this.messages) {
 426            for (int i = this.messages.size() - 1; i >= 0; --i) {
 427                final Message message = messages.get(i);
 428                final Jid mcp = message.getCounterpart();
 429                if (mcp == null) {
 430                    continue;
 431                }
 432                if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
 433                        && (carbon == message.isCarbon() || received)) {
 434                    final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
 435                    if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
 436                        return message;
 437                    } else {
 438                        return null;
 439                    }
 440                }
 441            }
 442        }
 443        return null;
 444    }
 445
 446    public Message findSentMessageWithUuid(String id) {
 447        synchronized (this.messages) {
 448            for (Message message : this.messages) {
 449                if (id.equals(message.getUuid())) {
 450                    return message;
 451                }
 452            }
 453        }
 454        return null;
 455    }
 456
 457    public Message findMessageWithRemoteId(String id, Jid counterpart) {
 458        synchronized (this.messages) {
 459            for (Message message : this.messages) {
 460                if (counterpart.equals(message.getCounterpart())
 461                        && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
 462                    return message;
 463                }
 464            }
 465        }
 466        return null;
 467    }
 468
 469    public Message findMessageWithServerMsgId(String id) {
 470        synchronized (this.messages) {
 471            for (Message message : this.messages) {
 472                if (id != null && id.equals(message.getServerMsgId())) {
 473                    return message;
 474                }
 475            }
 476        }
 477        return null;
 478    }
 479
 480    public boolean hasMessageWithCounterpart(Jid counterpart) {
 481        synchronized (this.messages) {
 482            for (Message message : this.messages) {
 483                if (counterpart.equals(message.getCounterpart())) {
 484                    return true;
 485                }
 486            }
 487        }
 488        return false;
 489    }
 490
 491    public void populateWithMessages(final List<Message> messages) {
 492        synchronized (this.messages) {
 493            messages.clear();
 494            messages.addAll(this.messages);
 495        }
 496        for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
 497            if (iterator.next().wasMergedIntoPrevious()) {
 498                iterator.remove();
 499            }
 500        }
 501    }
 502
 503    @Override
 504    public boolean isBlocked() {
 505        return getContact().isBlocked();
 506    }
 507
 508    @Override
 509    public boolean isDomainBlocked() {
 510        return getContact().isDomainBlocked();
 511    }
 512
 513    @Override
 514    public Jid getBlockedJid() {
 515        return getContact().getBlockedJid();
 516    }
 517
 518    public int countMessages() {
 519        synchronized (this.messages) {
 520            return this.messages.size();
 521        }
 522    }
 523
 524    public String getFirstMamReference() {
 525        return this.mFirstMamReference;
 526    }
 527
 528    public void setFirstMamReference(String reference) {
 529        this.mFirstMamReference = reference;
 530    }
 531
 532    public void setLastClearHistory(long time, String reference) {
 533        if (reference != null) {
 534            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
 535        } else {
 536            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
 537        }
 538    }
 539
 540    public MamReference getLastClearHistory() {
 541        return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
 542    }
 543
 544    public List<Jid> getAcceptedCryptoTargets() {
 545        if (mode == MODE_SINGLE) {
 546            return Collections.singletonList(getJid().asBareJid());
 547        } else {
 548            return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 549        }
 550    }
 551
 552    public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 553        setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 554    }
 555
 556    public boolean setCorrectingMessage(Message correctingMessage) {
 557        setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
 558        return correctingMessage == null && draftMessage != null;
 559    }
 560
 561    public Message getCorrectingMessage() {
 562        final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
 563        return uuid == null ? null : findSentMessageWithUuid(uuid);
 564    }
 565
 566    public boolean withSelf() {
 567        return getContact().isSelf();
 568    }
 569
 570    @Override
 571    public int compareTo(@NonNull Conversation another) {
 572        return ComparisonChain.start()
 573                .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
 574                .compare(another.getSortableTime(), getSortableTime())
 575                .result();
 576    }
 577
 578    private long getSortableTime() {
 579        Draft draft = getDraft();
 580        long messageTime = getLatestMessage().getTimeSent();
 581        if (draft == null) {
 582            return messageTime;
 583        } else {
 584            return Math.max(messageTime, draft.getTimestamp());
 585        }
 586    }
 587
 588    public String getDraftMessage() {
 589        return draftMessage;
 590    }
 591
 592    public void setDraftMessage(String draftMessage) {
 593        this.draftMessage = draftMessage;
 594    }
 595
 596    public boolean isRead() {
 597        synchronized (this.messages) {
 598            for(final Message message : Lists.reverse(this.messages)) {
 599                if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
 600                    continue;
 601                }
 602                return message.isRead();
 603            }
 604            return true;
 605        }
 606    }
 607
 608    public List<Message> markRead(String upToUuid) {
 609        final List<Message> unread = new ArrayList<>();
 610        synchronized (this.messages) {
 611            for (Message message : this.messages) {
 612                if (!message.isRead()) {
 613                    message.markRead();
 614                    unread.add(message);
 615                }
 616                if (message.getUuid().equals(upToUuid)) {
 617                    return unread;
 618                }
 619            }
 620        }
 621        return unread;
 622    }
 623
 624    public Message getLatestMessage() {
 625        synchronized (this.messages) {
 626            if (this.messages.size() == 0) {
 627                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 628                message.setType(Message.TYPE_STATUS);
 629                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 630                return message;
 631            } else {
 632                return this.messages.get(this.messages.size() - 1);
 633            }
 634        }
 635    }
 636
 637    public @NonNull
 638    CharSequence getName() {
 639        if (getMode() == MODE_MULTI) {
 640            final String roomName = getMucOptions().getName();
 641            final String subject = getMucOptions().getSubject();
 642            final Bookmark bookmark = getBookmark();
 643            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 644            if (printableValue(roomName)) {
 645                return roomName;
 646            } else if (printableValue(subject)) {
 647                return subject;
 648            } else if (printableValue(bookmarkName, false)) {
 649                return bookmarkName;
 650            } else {
 651                final String generatedName = getMucOptions().createNameFromParticipants();
 652                if (printableValue(generatedName)) {
 653                    return generatedName;
 654                } else {
 655                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
 656                }
 657            }
 658        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
 659            return contactJid;
 660        } else {
 661            return this.getContact().getDisplayName();
 662        }
 663    }
 664
 665    public String getAccountUuid() {
 666        return this.accountUuid;
 667    }
 668
 669    public Account getAccount() {
 670        return this.account;
 671    }
 672
 673    public void setAccount(final Account account) {
 674        this.account = account;
 675    }
 676
 677    public Contact getContact() {
 678        return this.account.getRoster().getContact(this.contactJid);
 679    }
 680
 681    @Override
 682    public Jid getJid() {
 683        return this.contactJid;
 684    }
 685
 686    public int getStatus() {
 687        return this.status;
 688    }
 689
 690    public void setStatus(int status) {
 691        this.status = status;
 692    }
 693
 694    public long getCreated() {
 695        return this.created;
 696    }
 697
 698    public ContentValues getContentValues() {
 699        ContentValues values = new ContentValues();
 700        values.put(UUID, uuid);
 701        values.put(NAME, name);
 702        values.put(CONTACT, contactUuid);
 703        values.put(ACCOUNT, accountUuid);
 704        values.put(CONTACTJID, contactJid.toString());
 705        values.put(CREATED, created);
 706        values.put(STATUS, status);
 707        values.put(MODE, mode);
 708        synchronized (this.attributes) {
 709            values.put(ATTRIBUTES, attributes.toString());
 710        }
 711        return values;
 712    }
 713
 714    public int getMode() {
 715        return this.mode;
 716    }
 717
 718    public void setMode(int mode) {
 719        this.mode = mode;
 720    }
 721
 722    /**
 723     * short for is Private and Non-anonymous
 724     */
 725    public boolean isSingleOrPrivateAndNonAnonymous() {
 726        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
 727    }
 728
 729    public boolean isPrivateAndNonAnonymous() {
 730        return getMucOptions().isPrivateAndNonAnonymous();
 731    }
 732
 733    public synchronized MucOptions getMucOptions() {
 734        if (this.mucOptions == null) {
 735            this.mucOptions = new MucOptions(this);
 736        }
 737        return this.mucOptions;
 738    }
 739
 740    public void resetMucOptions() {
 741        this.mucOptions = null;
 742    }
 743
 744    public void setContactJid(final Jid jid) {
 745        this.contactJid = jid;
 746    }
 747
 748    public Jid getNextCounterpart() {
 749        return this.nextCounterpart;
 750    }
 751
 752    public void setNextCounterpart(Jid jid) {
 753        this.nextCounterpart = jid;
 754    }
 755
 756    public int getNextEncryption() {
 757        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
 758            return Message.ENCRYPTION_NONE;
 759        }
 760        if (OmemoSetting.isAlways()) {
 761            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
 762        }
 763        final int defaultEncryption;
 764        if (suitableForOmemoByDefault(this)) {
 765            defaultEncryption = OmemoSetting.getEncryption();
 766        } else {
 767            defaultEncryption = Message.ENCRYPTION_NONE;
 768        }
 769        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
 770        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
 771            return defaultEncryption;
 772        } else {
 773            return encryption;
 774        }
 775    }
 776
 777    public boolean setNextEncryption(int encryption) {
 778        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
 779    }
 780
 781    public String getNextMessage() {
 782        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 783        return nextMessage == null ? "" : nextMessage;
 784    }
 785
 786    public @Nullable
 787    Draft getDraft() {
 788        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
 789        if (timestamp > getLatestMessage().getTimeSent()) {
 790            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 791            if (!TextUtils.isEmpty(message) && timestamp != 0) {
 792                return new Draft(message, timestamp);
 793            }
 794        }
 795        return null;
 796    }
 797
 798    public boolean setNextMessage(final String input) {
 799        final String message = input == null || input.trim().isEmpty() ? null : input;
 800        boolean changed = !getNextMessage().equals(message);
 801        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 802        if (changed) {
 803            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
 804        }
 805        return changed;
 806    }
 807
 808    public Bookmark getBookmark() {
 809        return this.account.getBookmark(this.contactJid);
 810    }
 811
 812    public Message findDuplicateMessage(Message message) {
 813        synchronized (this.messages) {
 814            for (int i = this.messages.size() - 1; i >= 0; --i) {
 815                if (this.messages.get(i).similar(message)) {
 816                    return this.messages.get(i);
 817                }
 818            }
 819        }
 820        return null;
 821    }
 822
 823    public boolean hasDuplicateMessage(Message message) {
 824        return findDuplicateMessage(message) != null;
 825    }
 826
 827    public Message findSentMessageWithBody(String body) {
 828        synchronized (this.messages) {
 829            for (int i = this.messages.size() - 1; i >= 0; --i) {
 830                Message message = this.messages.get(i);
 831                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 832                    String otherBody;
 833                    if (message.hasFileOnRemoteHost()) {
 834                        otherBody = message.getFileParams().url;
 835                    } else {
 836                        otherBody = message.body;
 837                    }
 838                    if (otherBody != null && otherBody.equals(body)) {
 839                        return message;
 840                    }
 841                }
 842            }
 843            return null;
 844        }
 845    }
 846
 847    public Message findRtpSession(final String sessionId, final int s) {
 848        synchronized (this.messages) {
 849            for (int i = this.messages.size() - 1; i >= 0; --i) {
 850                final Message message = this.messages.get(i);
 851                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
 852                    return message;
 853                }
 854            }
 855        }
 856        return null;
 857    }
 858
 859    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
 860        if (serverMsgId == null || remoteMsgId == null) {
 861            return false;
 862        }
 863        synchronized (this.messages) {
 864            for (Message message : this.messages) {
 865                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
 866                    return true;
 867                }
 868            }
 869        }
 870        return false;
 871    }
 872
 873    public MamReference getLastMessageTransmitted() {
 874        final MamReference lastClear = getLastClearHistory();
 875        MamReference lastReceived = new MamReference(0);
 876        synchronized (this.messages) {
 877            for (int i = this.messages.size() - 1; i >= 0; --i) {
 878                final Message message = this.messages.get(i);
 879                if (message.isPrivateMessage()) {
 880                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 881                }
 882                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 883                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
 884                    break;
 885                }
 886            }
 887        }
 888        return MamReference.max(lastClear, lastReceived);
 889    }
 890
 891    public void setMutedTill(long value) {
 892        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 893    }
 894
 895    public boolean isMuted() {
 896        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 897    }
 898
 899    public boolean alwaysNotify() {
 900        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
 901    }
 902
 903    public boolean setAttribute(String key, boolean value) {
 904        return setAttribute(key, String.valueOf(value));
 905    }
 906
 907    private boolean setAttribute(String key, long value) {
 908        return setAttribute(key, Long.toString(value));
 909    }
 910
 911    private boolean setAttribute(String key, int value) {
 912        return setAttribute(key, String.valueOf(value));
 913    }
 914
 915    public boolean setAttribute(String key, String value) {
 916        synchronized (this.attributes) {
 917            try {
 918                if (value == null) {
 919                    if (this.attributes.has(key)) {
 920                        this.attributes.remove(key);
 921                        return true;
 922                    } else {
 923                        return false;
 924                    }
 925                } else {
 926                    final String prev = this.attributes.optString(key, null);
 927                    this.attributes.put(key, value);
 928                    return !value.equals(prev);
 929                }
 930            } catch (JSONException e) {
 931                throw new AssertionError(e);
 932            }
 933        }
 934    }
 935
 936    public boolean setAttribute(String key, List<Jid> jids) {
 937        JSONArray array = new JSONArray();
 938        for (Jid jid : jids) {
 939            array.put(jid.asBareJid().toString());
 940        }
 941        synchronized (this.attributes) {
 942            try {
 943                this.attributes.put(key, array);
 944                return true;
 945            } catch (JSONException e) {
 946                return false;
 947            }
 948        }
 949    }
 950
 951    public String getAttribute(String key) {
 952        synchronized (this.attributes) {
 953            return this.attributes.optString(key, null);
 954        }
 955    }
 956
 957    private List<Jid> getJidListAttribute(String key) {
 958        ArrayList<Jid> list = new ArrayList<>();
 959        synchronized (this.attributes) {
 960            try {
 961                JSONArray array = this.attributes.getJSONArray(key);
 962                for (int i = 0; i < array.length(); ++i) {
 963                    try {
 964                        list.add(Jid.of(array.getString(i)));
 965                    } catch (IllegalArgumentException e) {
 966                        //ignored
 967                    }
 968                }
 969            } catch (JSONException e) {
 970                //ignored
 971            }
 972        }
 973        return list;
 974    }
 975
 976    private int getIntAttribute(String key, int defaultValue) {
 977        String value = this.getAttribute(key);
 978        if (value == null) {
 979            return defaultValue;
 980        } else {
 981            try {
 982                return Integer.parseInt(value);
 983            } catch (NumberFormatException e) {
 984                return defaultValue;
 985            }
 986        }
 987    }
 988
 989    public long getLongAttribute(String key, long defaultValue) {
 990        String value = this.getAttribute(key);
 991        if (value == null) {
 992            return defaultValue;
 993        } else {
 994            try {
 995                return Long.parseLong(value);
 996            } catch (NumberFormatException e) {
 997                return defaultValue;
 998            }
 999        }
1000    }
1001
1002    public boolean getBooleanAttribute(String key, boolean defaultValue) {
1003        String value = this.getAttribute(key);
1004        if (value == null) {
1005            return defaultValue;
1006        } else {
1007            return Boolean.parseBoolean(value);
1008        }
1009    }
1010
1011    public void add(Message message) {
1012        synchronized (this.messages) {
1013            this.messages.add(message);
1014        }
1015    }
1016
1017    public void prepend(int offset, Message message) {
1018        synchronized (this.messages) {
1019            this.messages.add(Math.min(offset, this.messages.size()), message);
1020        }
1021    }
1022
1023    public void addAll(int index, List<Message> messages) {
1024        synchronized (this.messages) {
1025            this.messages.addAll(index, messages);
1026        }
1027        account.getPgpDecryptionService().decrypt(messages);
1028    }
1029
1030    public void expireOldMessages(long timestamp) {
1031        synchronized (this.messages) {
1032            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
1033                if (iterator.next().getTimeSent() < timestamp) {
1034                    iterator.remove();
1035                }
1036            }
1037            untieMessages();
1038        }
1039    }
1040
1041    public void sort() {
1042        synchronized (this.messages) {
1043            Collections.sort(this.messages, (left, right) -> {
1044                if (left.getTimeSent() < right.getTimeSent()) {
1045                    return -1;
1046                } else if (left.getTimeSent() > right.getTimeSent()) {
1047                    return 1;
1048                } else {
1049                    return 0;
1050                }
1051            });
1052            untieMessages();
1053        }
1054    }
1055
1056    private void untieMessages() {
1057        for (Message message : this.messages) {
1058            message.untie();
1059        }
1060    }
1061
1062    public int unreadCount() {
1063        synchronized (this.messages) {
1064            int count = 0;
1065            for(final Message message : Lists.reverse(this.messages)) {
1066                if (message.isRead()) {
1067                    if (message.getType() == Message.TYPE_RTP_SESSION) {
1068                        continue;
1069                    }
1070                    return count;
1071                }
1072                ++count;
1073            }
1074            return count;
1075        }
1076    }
1077
1078    public int receivedMessagesCount() {
1079        int count = 0;
1080        synchronized (this.messages) {
1081            for (Message message : messages) {
1082                if (message.getStatus() == Message.STATUS_RECEIVED) {
1083                    ++count;
1084                }
1085            }
1086        }
1087        return count;
1088    }
1089
1090    public int sentMessagesCount() {
1091        int count = 0;
1092        synchronized (this.messages) {
1093            for (Message message : messages) {
1094                if (message.getStatus() != Message.STATUS_RECEIVED) {
1095                    ++count;
1096                }
1097            }
1098        }
1099        return count;
1100    }
1101
1102    public boolean isWithStranger() {
1103        final Contact contact = getContact();
1104        return mode == MODE_SINGLE
1105                && !contact.isOwnServer()
1106                && !contact.showInContactList()
1107                && !contact.isSelf()
1108                && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
1109                && sentMessagesCount() == 0;
1110    }
1111
1112    public int getReceivedMessagesCountSinceUuid(String uuid) {
1113        if (uuid == null) {
1114            return 0;
1115        }
1116        int count = 0;
1117        synchronized (this.messages) {
1118            for (int i = messages.size() - 1; i >= 0; i--) {
1119                final Message message = messages.get(i);
1120                if (uuid.equals(message.getUuid())) {
1121                    return count;
1122                }
1123                if (message.getStatus() <= Message.STATUS_RECEIVED) {
1124                    ++count;
1125                }
1126            }
1127        }
1128        return 0;
1129    }
1130
1131    @Override
1132    public int getAvatarBackgroundColor() {
1133        return UIHelper.getColorForName(getName().toString());
1134    }
1135
1136    @Override
1137    public String getAvatarName() {
1138        return getName().toString();
1139    }
1140
1141    public void setCurrentTab(int tab) {
1142        mCurrentTab = tab;
1143    }
1144
1145    public int getCurrentTab() {
1146        if (mCurrentTab >= 0) return mCurrentTab;
1147
1148        if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
1149            return 0;
1150        }
1151
1152        return 1;
1153    }
1154
1155    public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1156        pagerAdapter.startCommand(command, xmppConnectionService);
1157    }
1158
1159    public void setupViewPager(ViewPager pager, TabLayout tabs) {
1160        pagerAdapter.setupViewPager(pager, tabs);
1161    }
1162
1163    public interface OnMessageFound {
1164        void onMessageFound(final Message message);
1165    }
1166
1167    public static class Draft {
1168        private final String message;
1169        private final long timestamp;
1170
1171        private Draft(String message, long timestamp) {
1172            this.message = message;
1173            this.timestamp = timestamp;
1174        }
1175
1176        public long getTimestamp() {
1177            return timestamp;
1178        }
1179
1180        public String getMessage() {
1181            return message;
1182        }
1183    }
1184
1185    public class ConversationPagerAdapter extends PagerAdapter {
1186        protected ViewPager mPager = null;
1187        protected TabLayout mTabs = null;
1188        ArrayList<CommandSession> sessions = new ArrayList<>();
1189
1190        public void setupViewPager(ViewPager pager, TabLayout tabs) {
1191            mPager = pager;
1192            mTabs = tabs;
1193            pager.setAdapter(this);
1194            tabs.setupWithViewPager(mPager);
1195            pager.setCurrentItem(getCurrentTab());
1196
1197            mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1198                public void onPageScrollStateChanged(int state) { }
1199                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1200
1201                public void onPageSelected(int position) {
1202                    setCurrentTab(position);
1203                }
1204            });
1205        }
1206
1207        public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1208            CommandSession session = new CommandSession(command.getAttribute("name"), xmppConnectionService);
1209
1210            final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1211            packet.setTo(command.getAttributeAsJid("jid"));
1212            final Element c = packet.addChild("command", Namespace.COMMANDS);
1213            c.setAttribute("node", command.getAttribute("node"));
1214            c.setAttribute("action", "execute");
1215            xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1216                mPager.post(() -> {
1217                    session.updateWithResponse(iq);
1218                });
1219            });
1220
1221            sessions.add(session);
1222            notifyDataSetChanged();
1223            mPager.setCurrentItem(getCount() - 1);
1224        }
1225
1226        @NonNull
1227        @Override
1228        public Object instantiateItem(@NonNull ViewGroup container, int position) {
1229            if (position < 2) {
1230              return mPager.getChildAt(position);
1231            }
1232
1233            CommandSession session = sessions.get(position-2);
1234            CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
1235            container.addView(binding.getRoot());
1236            binding.form.setAdapter(session);
1237            binding.done.setOnClickListener((button) -> {
1238                if (session.execute()) {
1239                    sessions.remove(session);
1240                    notifyDataSetChanged();
1241                }
1242            });
1243
1244            session.setBinding(binding);
1245            return session;
1246        }
1247
1248        @Override
1249        public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1250            if (position < 2) return;
1251
1252            container.removeView(((CommandSession) o).getView());
1253        }
1254
1255        @Override
1256        public int getItemPosition(Object o) {
1257            if (o == mPager.getChildAt(0)) return PagerAdapter.POSITION_UNCHANGED;
1258            if (o == mPager.getChildAt(1)) return PagerAdapter.POSITION_UNCHANGED;
1259
1260            int pos = sessions.indexOf(o);
1261            if (pos < 0) return PagerAdapter.POSITION_NONE;
1262            return pos + 2;
1263        }
1264
1265        @Override
1266        public int getCount() {
1267            int count = 2 + sessions.size();
1268            if (count > 2) {
1269                mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1270            } else {
1271                mTabs.setTabMode(TabLayout.MODE_FIXED);
1272            }
1273            return count;
1274        }
1275
1276        @Override
1277        public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1278            if (view == o) return true;
1279
1280            if (o instanceof CommandSession) {
1281                return ((CommandSession) o).getView() == view;
1282            }
1283
1284            return false;
1285        }
1286
1287        @Nullable
1288        @Override
1289        public CharSequence getPageTitle(int position) {
1290            switch (position) {
1291                case 0:
1292                    return "Conversation";
1293                case 1:
1294                    return "Commands";
1295                default:
1296                    CommandSession session = sessions.get(position-2);
1297                    if (session == null) return super.getPageTitle(position);
1298                    return session.getTitle();
1299            }
1300        }
1301
1302        class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
1303            abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1304                protected T binding;
1305
1306                public ViewHolder(T binding) {
1307                    super(binding.getRoot());
1308                    this.binding = binding;
1309                }
1310
1311                abstract public void bind(Element el);
1312            }
1313
1314            class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1315                public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1316
1317                @Override
1318                public void bind(Element iq) {
1319                    binding.errorIcon.setVisibility(View.VISIBLE);
1320
1321                    Element error = iq.findChild("error");
1322                    if (error == null) return;
1323                    String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1324                    if (text == null || text.equals("")) {
1325                        text = error.getChildren().get(0).getName();
1326                    }
1327                    binding.message.setText(text);
1328                }
1329            }
1330
1331            class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1332                public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1333
1334                @Override
1335                public void bind(Element note) {
1336                    binding.message.setText(note.getContent());
1337
1338                    String type = note.getAttribute("type");
1339                    if (type != null && type.equals("error")) {
1340                        binding.errorIcon.setVisibility(View.VISIBLE);
1341                    }
1342                }
1343            }
1344
1345            class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1346                public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1347
1348                @Override
1349                public void bind(Element field) {
1350                    String label = field.getAttribute("label");
1351                    if (label == null) label = field.getAttribute("var");
1352                    if (label == null) {
1353                        binding.label.setVisibility(View.GONE);
1354                    } else {
1355                        binding.label.setVisibility(View.VISIBLE);
1356                        binding.label.setText(label);
1357                    }
1358
1359                    String desc = field.findChildContent("desc", "jabber:x:data");
1360                    if (desc == null) {
1361                        binding.desc.setVisibility(View.GONE);
1362                    } else {
1363                        binding.desc.setVisibility(View.VISIBLE);
1364                        binding.desc.setText(desc);
1365                    }
1366
1367                    ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
1368                    for (Element el : field.getChildren()) {
1369                        if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1370                            values.add(el.getContent());
1371                        }
1372                    }
1373                    binding.values.setAdapter(values);
1374                }
1375            }
1376
1377            class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
1378                public TextFieldViewHolder(CommandTextFieldBinding binding) {
1379                    super(binding);
1380                    binding.textinput.addTextChangedListener(this);
1381                }
1382                protected Element mValue = null;
1383
1384                @Override
1385                public void bind(Element field) {
1386                    String label = field.getAttribute("label");
1387                    if (label == null) label = field.getAttribute("var");
1388                    if (label == null) label = "";
1389                    binding.textinputLayout.setHint(label);
1390
1391                    String desc = field.findChildContent("desc", "jabber:x:data");
1392                    if (desc == null) {
1393                        binding.desc.setVisibility(View.GONE);
1394                    } else {
1395                        binding.desc.setVisibility(View.VISIBLE);
1396                        binding.desc.setText(desc);
1397                    }
1398
1399                    mValue = field.findChild("value", "jabber:x:data");
1400                    if (mValue == null) {
1401                        mValue = field.addChild("value", "jabber:x:data");
1402                    }
1403                    binding.textinput.setText(mValue.getContent());
1404
1405                    binding.textinput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1406                    Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1407                    if (validate == null) return;
1408                    String datatype = validate.getAttribute("datatype");
1409
1410                    if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
1411                        binding.textinput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
1412                    }
1413
1414                    if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
1415                        binding.textinput.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
1416                    }
1417
1418                    if (datatype.equals("xs:date")) {
1419                        binding.textinput.setInputType(InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
1420                    }
1421
1422                    if (datatype.equals("xs:dateTime")) {
1423                        binding.textinput.setInputType(InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
1424                    }
1425
1426                    if (datatype.equals("xs:time")) {
1427                        binding.textinput.setInputType(InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
1428                    }
1429
1430                    if (datatype.equals("xs:anyURI")) {
1431                        binding.textinput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
1432                    }
1433                }
1434
1435                @Override
1436                public void afterTextChanged(Editable s) {
1437                    if (mValue == null) return;
1438
1439                    mValue.setContent(s.toString());
1440                }
1441
1442                @Override
1443                public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1444
1445                @Override
1446                public void onTextChanged(CharSequence s, int start, int count, int after) { }
1447            }
1448
1449            class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
1450                public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
1451
1452                @Override
1453                public void bind(Element oob) {
1454                    binding.webview.getSettings().setJavaScriptEnabled(true);
1455                    binding.webview.setWebViewClient(new WebViewClient() {
1456                        @Override
1457                        public void onPageFinished(WebView view, String url) {
1458                            super.onPageFinished(view, url);
1459                            mTitle = view.getTitle();
1460                            ConversationPagerAdapter.this.notifyDataSetChanged();
1461                        }
1462                    });
1463                    binding.webview.loadUrl(oob.findChildContent("url", "jabber:x:oob"));
1464                }
1465            }
1466
1467            final int TYPE_ERROR = 1;
1468            final int TYPE_NOTE = 2;
1469            final int TYPE_WEB = 3;
1470            final int TYPE_RESULT_FIELD = 4;
1471            final int TYPE_TEXT_FIELD = 5;
1472
1473            protected String mTitle;
1474            protected CommandPageBinding mBinding = null;
1475            protected IqPacket response = null;
1476            protected Element responseElement = null;
1477            protected XmppConnectionService xmppConnectionService;
1478
1479            CommandSession(String title, XmppConnectionService xmppConnectionService) {
1480                mTitle = title;
1481                this.xmppConnectionService = xmppConnectionService;
1482            }
1483
1484            public String getTitle() {
1485                return mTitle;
1486            }
1487
1488            public void updateWithResponse(IqPacket iq) {
1489                this.responseElement = null;
1490                this.response = iq;
1491
1492                Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1493                if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1494                    for (Element el : command.getChildren()) {
1495                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1496                            String title = el.findChildContent("title", "jabber:x:data");
1497                            if (title != null) {
1498                                mTitle = title;
1499                                ConversationPagerAdapter.this.notifyDataSetChanged();
1500                            }
1501                            this.responseElement = el;
1502                            break;
1503                        }
1504                        if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1505                            String url = el.findChildContent("url", "jabber:x:oob");
1506                            if (url != null) {
1507                                String scheme = Uri.parse(url).getScheme();
1508                                if (scheme.equals("http") || scheme.equals("https")) {
1509                                    this.responseElement = el;
1510                                    break;
1511                                }
1512                            }
1513                        }
1514                        if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1515                            this.responseElement = el;
1516                            break;
1517                        }
1518                    }
1519                }
1520
1521                notifyDataSetChanged();
1522            }
1523
1524            @Override
1525            public int getItemCount() {
1526                if (response == null) return 0;
1527                if (response.getType() == IqPacket.TYPE.RESULT && responseElement.getNamespace().equals("jabber:x:data")) {
1528                    int i = 0;
1529                    for (Element el : responseElement.getChildren()) {
1530                        if (!el.getNamespace().equals("jabber:x:data")) continue;
1531                        if (el.getName().equals("title")) continue;
1532                        if (el.getName().equals("field")) {
1533                            String type = el.getAttribute("type");
1534                            if (type != null && type.equals("hidden")) continue;
1535                        }
1536
1537                        i++;
1538                    }
1539                    return i;
1540                }
1541                return 1;
1542            }
1543
1544            public Element getItem(int position) {
1545                if (response == null) return null;
1546
1547                if (response.getType() == IqPacket.TYPE.RESULT) {
1548                    if (responseElement.getNamespace().equals("jabber:x:data")) {
1549                        int i = 0;
1550                        for (Element el : responseElement.getChildren()) {
1551                            if (!el.getNamespace().equals("jabber:x:data")) continue;
1552                            if (el.getName().equals("title")) continue;
1553                            if (el.getName().equals("field")) {
1554                                String type = el.getAttribute("type");
1555                                if (type != null && type.equals("hidden")) continue;
1556                            }
1557
1558                            if (i < position) {
1559                                i++;
1560                                continue;
1561                            }
1562
1563                            return el;
1564                        }
1565                    }
1566                }
1567
1568                return responseElement == null ? response : responseElement;
1569            }
1570
1571            @Override
1572            public int getItemViewType(int position) {
1573                if (response == null) return -1;
1574
1575                if (response.getType() == IqPacket.TYPE.RESULT) {
1576                    Element item = getItem(position);
1577                    if (item.getName().equals("note")) return TYPE_NOTE;
1578                    if (item.getNamespace().equals("jabber:x:oob")) return TYPE_WEB;
1579                    if (item.getName().equals("instructions") && item.getNamespace().equals("jabber:x:data")) return TYPE_NOTE;
1580                    if (item.getName().equals("field") && item.getNamespace().equals("jabber:x:data")) {
1581                        String formType = responseElement.getAttribute("type");
1582                        String fieldType = item.getAttribute("type");
1583                        if (formType.equals("result") || fieldType.equals("fixed")) return TYPE_RESULT_FIELD;
1584                        if (formType.equals("form")) return TYPE_TEXT_FIELD;
1585                    }
1586                    return -1;
1587                } else {
1588                    return TYPE_ERROR;
1589                }
1590            }
1591
1592            @Override
1593            public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
1594                switch(viewType) {
1595                    case TYPE_ERROR: {
1596                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1597                        return new ErrorViewHolder(binding);
1598                    }
1599                    case TYPE_NOTE: {
1600                        CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1601                        return new NoteViewHolder(binding);
1602                    }
1603                    case TYPE_WEB: {
1604                        CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
1605                        return new WebViewHolder(binding);
1606                    }
1607                    case TYPE_RESULT_FIELD: {
1608                        CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
1609                        return new ResultFieldViewHolder(binding);
1610                    }
1611                    case TYPE_TEXT_FIELD: {
1612                        CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
1613                        return new TextFieldViewHolder(binding);
1614                    }
1615                    default:
1616                        throw new IllegalArgumentException("Unknown viewType: " + viewType);
1617                }
1618            }
1619
1620            @Override
1621            public void onBindViewHolder(ViewHolder viewHolder, int position) {
1622                viewHolder.bind(getItem(position));
1623            }
1624
1625            public View getView() {
1626                return mBinding.getRoot();
1627            }
1628
1629            public boolean execute() {
1630                if (response == null || responseElement == null) return true;
1631                Element command = response.findChild("command", "http://jabber.org/protocol/commands");
1632                if (command == null) return true;
1633                String status = command.getAttribute("status");
1634                if (status == null || !status.equals("executing")) return true;
1635                if (!responseElement.getName().equals("x") || !responseElement.getNamespace().equals("jabber:x:data")) return true;
1636                String formType = responseElement.getAttribute("type");
1637                if (formType == null || !formType.equals("form")) return true;
1638
1639                responseElement.setAttribute("type", "submit");
1640
1641                final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1642                packet.setTo(response.getFrom());
1643                final Element c = packet.addChild("command", Namespace.COMMANDS);
1644                c.setAttribute("node", command.getAttribute("node"));
1645                c.setAttribute("sessionid", command.getAttribute("sessionid"));
1646                c.setAttribute("action", "execute");
1647                c.addChild(responseElement);
1648
1649                xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1650                    getView().post(() -> {
1651                        updateWithResponse(iq);
1652                    });
1653                });
1654
1655                return false;
1656            }
1657
1658            public void setBinding(CommandPageBinding b) {
1659                mBinding = b;
1660                mBinding.form.setLayoutManager(new LinearLayoutManager(mPager.getContext()) {
1661                    @Override
1662                    public boolean canScrollVertically() { return getItemCount() > 1; }
1663                });
1664            }
1665        }
1666    }
1667}