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