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