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