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