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