Conversation.java

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