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