Conversation.java

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