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 setTextOrHide(binding.text, cell.reported.getLabel());
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 String getVar() {
1730 return el.getAttribute("var");
1731 }
1732
1733 public Optional<String> getLabel() {
1734 String label = el.getAttribute("label");
1735 if (label == null) label = getVar();
1736 return Optional.ofNullable(label);
1737 }
1738
1739 public Optional<String> getDesc() {
1740 return Optional.ofNullable(el.findChildContent("desc", "jabber:x:data"));
1741 }
1742
1743 public Element getValue() {
1744 Element value = el.findChild("value", "jabber:x:data");
1745 if (value == null) {
1746 value = el.addChild("value", "jabber:x:data");
1747 }
1748 return value;
1749 }
1750
1751 public List<Option> getOptions() {
1752 return Option.forField(el);
1753 }
1754 }
1755
1756 class Cell extends Item {
1757 protected Field reported;
1758
1759 Cell(Field reported, Element item) {
1760 super(item, TYPE_RESULT_CELL);
1761 this.reported = reported;
1762 }
1763 }
1764
1765 protected Field mkField(Element el) {
1766 int viewType = -1;
1767
1768 String formType = responseElement.getAttribute("type");
1769 if (formType != null) {
1770 String fieldType = el.getAttribute("type");
1771 if (fieldType == null) fieldType = "text-single";
1772
1773 if (formType.equals("result") || fieldType.equals("fixed")) {
1774 viewType = TYPE_RESULT_FIELD;
1775 } else if (formType.equals("form")) {
1776 if (fieldType.equals("boolean")) {
1777 viewType = TYPE_CHECKBOX_FIELD;
1778 } else if (fieldType.equals("list-single")) {
1779 Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1780 if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
1781 viewType = TYPE_RADIO_EDIT_FIELD;
1782 } else {
1783 viewType = TYPE_SPINNER_FIELD;
1784 }
1785 } else {
1786 viewType = TYPE_TEXT_FIELD;
1787 }
1788 }
1789
1790 return new Field(el, viewType);
1791 }
1792
1793 return null;
1794 }
1795
1796 protected Item mkItem(Element el, int pos) {
1797 int viewType = -1;
1798
1799 if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
1800 if (el.getName().equals("note")) {
1801 viewType = TYPE_NOTE;
1802 } else if (el.getNamespace().equals("jabber:x:oob")) {
1803 viewType = TYPE_WEB;
1804 } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
1805 viewType = TYPE_NOTE;
1806 } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
1807 Field field = mkField(el);
1808 if (field != null) {
1809 items.put(pos, field);
1810 return field;
1811 }
1812 }
1813 } else if (response != null) {
1814 viewType = TYPE_ERROR;
1815 }
1816
1817 Item item = new Item(el, viewType);
1818 items.put(pos, item);
1819 return item;
1820 }
1821
1822 final int TYPE_ERROR = 1;
1823 final int TYPE_NOTE = 2;
1824 final int TYPE_WEB = 3;
1825 final int TYPE_RESULT_FIELD = 4;
1826 final int TYPE_TEXT_FIELD = 5;
1827 final int TYPE_CHECKBOX_FIELD = 6;
1828 final int TYPE_SPINNER_FIELD = 7;
1829 final int TYPE_RADIO_EDIT_FIELD = 8;
1830 final int TYPE_RESULT_CELL = 9;
1831
1832 protected String mTitle;
1833 protected CommandPageBinding mBinding = null;
1834 protected IqPacket response = null;
1835 protected Element responseElement = null;
1836 protected List<Field> reported = null;
1837 protected SparseArray<Item> items = new SparseArray<>();
1838 protected XmppConnectionService xmppConnectionService;
1839 protected ArrayAdapter<String> actionsAdapter;
1840 protected GridLayoutManager layoutManager;
1841
1842 CommandSession(String title, XmppConnectionService xmppConnectionService) {
1843 mTitle = title;
1844 this.xmppConnectionService = xmppConnectionService;
1845 setupLayoutManager();
1846 actionsAdapter = new ArrayAdapter<String>(xmppConnectionService, R.layout.simple_list_item) {
1847 @Override
1848 public View getView(int position, View convertView, ViewGroup parent) {
1849 View v = super.getView(position, convertView, parent);
1850 TextView tv = (TextView) v.findViewById(android.R.id.text1);
1851 tv.setGravity(Gravity.CENTER);
1852 int resId = xmppConnectionService.getResources().getIdentifier("action_" + tv.getText() , "string" , xmppConnectionService.getPackageName());
1853 if (resId != 0) tv.setText(xmppConnectionService.getResources().getString(resId));
1854 tv.setTextColor(ContextCompat.getColor(xmppConnectionService, R.color.white));
1855 tv.setBackgroundColor(UIHelper.getColorForName(tv.getText().toString()));
1856 return v;
1857 }
1858 };
1859 actionsAdapter.registerDataSetObserver(new DataSetObserver() {
1860 @Override
1861 public void onChanged() {
1862 if (mBinding == null) return;
1863
1864 mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
1865 }
1866
1867 @Override
1868 public void onInvalidated() {}
1869 });
1870 }
1871
1872 public String getTitle() {
1873 return mTitle;
1874 }
1875
1876 public void updateWithResponse(IqPacket iq) {
1877 this.responseElement = null;
1878 this.reported = null;
1879 this.response = iq;
1880 this.items.clear();
1881 this.actionsAdapter.clear();
1882 layoutManager.setSpanCount(1);
1883
1884 Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1885 if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1886 for (Element el : command.getChildren()) {
1887 if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1888 for (Element action : el.getChildren()) {
1889 if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
1890 if (action.getName().equals("execute")) continue;
1891
1892 actionsAdapter.add(action.getName());
1893 }
1894 }
1895 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1896 String title = el.findChildContent("title", "jabber:x:data");
1897 if (title != null) {
1898 mTitle = title;
1899 ConversationPagerAdapter.this.notifyDataSetChanged();
1900 }
1901
1902 if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
1903 this.responseElement = el;
1904 setupReported(el.findChild("reported", "jabber:x:data"));
1905 layoutManager.setSpanCount(this.reported == null ? 1 : this.reported.size());
1906 }
1907 break;
1908 }
1909 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1910 String url = el.findChildContent("url", "jabber:x:oob");
1911 if (url != null) {
1912 String scheme = Uri.parse(url).getScheme();
1913 if (scheme.equals("http") || scheme.equals("https")) {
1914 this.responseElement = el;
1915 break;
1916 }
1917 }
1918 }
1919 if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1920 this.responseElement = el;
1921 break;
1922 }
1923 }
1924
1925 if (responseElement == null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
1926 removeSession(this);
1927 return;
1928 }
1929
1930 if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
1931 // No actions have been given, but we are not done?
1932 // This is probably a spec violation, but we should do *something*
1933 actionsAdapter.add("execute");
1934 }
1935 }
1936
1937 if (actionsAdapter.getCount() > 0) {
1938 if (actionsAdapter.getPosition("cancel") < 0) actionsAdapter.insert("cancel", 0);
1939 } else {
1940 actionsAdapter.add("close");
1941 }
1942
1943 notifyDataSetChanged();
1944 }
1945
1946 protected void setupReported(Element el) {
1947 if (el == null) {
1948 reported = null;
1949 return;
1950 }
1951
1952 reported = new ArrayList<>();
1953 for (Element fieldEl : el.getChildren()) {
1954 if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
1955 reported.add(mkField(fieldEl));
1956 }
1957 }
1958
1959 @Override
1960 public int getItemCount() {
1961 if (response == null) return 0;
1962 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && 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 if (reported != null) i += reported.size();
1974 continue;
1975 }
1976
1977 i++;
1978 }
1979 return i;
1980 }
1981 return 1;
1982 }
1983
1984 public Item getItem(int position) {
1985 if (items.get(position) != null) return items.get(position);
1986 if (response == null) return null;
1987
1988 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
1989 if (responseElement.getNamespace().equals("jabber:x:data")) {
1990 int i = 0;
1991 for (Element el : responseElement.getChildren()) {
1992 if (!el.getNamespace().equals("jabber:x:data")) continue;
1993 if (el.getName().equals("title")) continue;
1994 if (el.getName().equals("field")) {
1995 String type = el.getAttribute("type");
1996 if (type != null && type.equals("hidden")) continue;
1997 }
1998
1999 if (el.getName().equals("reported") || el.getName().equals("item")) {
2000 Cell cell = null;
2001
2002 if (reported != null) {
2003 if (reported.size() > position - i) {
2004 Field reportedField = reported.get(position - i);
2005 Element itemField = null;
2006 if (el.getName().equals("item")) {
2007 for (Element subel : el.getChildren()) {
2008 if (subel.getAttribute("var").equals(reportedField.getVar())) {
2009 itemField = subel;
2010 break;
2011 }
2012 }
2013 }
2014 cell = new Cell(reportedField, itemField);
2015 } else {
2016 i += reported.size();
2017 continue;
2018 }
2019 }
2020
2021 if (cell != null) {
2022 items.put(position, cell);
2023 return cell;
2024 }
2025 }
2026
2027 if (i < position) {
2028 i++;
2029 continue;
2030 }
2031
2032 return mkItem(el, position);
2033 }
2034 }
2035 }
2036
2037 return mkItem(responseElement == null ? response : responseElement, position);
2038 }
2039
2040 @Override
2041 public int getItemViewType(int position) {
2042 return getItem(position).viewType;
2043 }
2044
2045 @Override
2046 public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2047 switch(viewType) {
2048 case TYPE_ERROR: {
2049 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2050 return new ErrorViewHolder(binding);
2051 }
2052 case TYPE_NOTE: {
2053 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2054 return new NoteViewHolder(binding);
2055 }
2056 case TYPE_WEB: {
2057 CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2058 return new WebViewHolder(binding);
2059 }
2060 case TYPE_RESULT_FIELD: {
2061 CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2062 return new ResultFieldViewHolder(binding);
2063 }
2064 case TYPE_RESULT_CELL: {
2065 CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2066 return new ResultCellViewHolder(binding);
2067 }
2068 case TYPE_CHECKBOX_FIELD: {
2069 CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2070 return new CheckboxFieldViewHolder(binding);
2071 }
2072 case TYPE_RADIO_EDIT_FIELD: {
2073 CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2074 return new RadioEditFieldViewHolder(binding);
2075 }
2076 case TYPE_SPINNER_FIELD: {
2077 CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2078 return new SpinnerFieldViewHolder(binding);
2079 }
2080 case TYPE_TEXT_FIELD: {
2081 CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2082 return new TextFieldViewHolder(binding);
2083 }
2084 default:
2085 throw new IllegalArgumentException("Unknown viewType: " + viewType);
2086 }
2087 }
2088
2089 @Override
2090 public void onBindViewHolder(ViewHolder viewHolder, int position) {
2091 viewHolder.bind(getItem(position));
2092 }
2093
2094 public View getView() {
2095 return mBinding.getRoot();
2096 }
2097
2098 public boolean validate() {
2099 int count = getItemCount();
2100 boolean isValid = true;
2101 for (int i = 0; i < count; i++) {
2102 boolean oneIsValid = getItem(i).validate();
2103 isValid = isValid && oneIsValid;
2104 }
2105 notifyDataSetChanged();
2106 return isValid;
2107 }
2108
2109 public boolean execute() {
2110 return execute("execute");
2111 }
2112
2113 public boolean execute(int actionPosition) {
2114 return execute(actionsAdapter.getItem(actionPosition));
2115 }
2116
2117 public boolean execute(String action) {
2118 if (!action.equals("cancel") && !validate()) return false;
2119 if (response == null) return true;
2120 Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2121 if (command == null) return true;
2122 String status = command.getAttribute("status");
2123 if (status == null || !status.equals("executing")) return true;
2124
2125 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2126 packet.setTo(response.getFrom());
2127 final Element c = packet.addChild("command", Namespace.COMMANDS);
2128 c.setAttribute("node", command.getAttribute("node"));
2129 c.setAttribute("sessionid", command.getAttribute("sessionid"));
2130 c.setAttribute("action", action);
2131
2132 String formType = responseElement == null ? null : responseElement.getAttribute("type");
2133 if (!action.equals("cancel") &&
2134 responseElement != null &&
2135 responseElement.getName().equals("x") &&
2136 responseElement.getNamespace().equals("jabber:x:data") &&
2137 formType != null && formType.equals("form")) {
2138
2139 responseElement.setAttribute("type", "submit");
2140 c.addChild(responseElement);
2141 }
2142
2143 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2144 getView().post(() -> {
2145 updateWithResponse(iq);
2146 });
2147 });
2148
2149 return false;
2150 }
2151
2152 protected GridLayoutManager setupLayoutManager() {
2153 layoutManager = new GridLayoutManager(mPager.getContext(), layoutManager == null ? 1 : layoutManager.getSpanCount()) {
2154 @Override
2155 public boolean canScrollVertically() { return getItemCount() > 1; }
2156 };
2157 layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2158 @Override
2159 public int getSpanSize(int position) {
2160 if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2161 return 1;
2162 }
2163 });
2164 return layoutManager;
2165 }
2166
2167 public void setBinding(CommandPageBinding b) {
2168 mBinding = b;
2169 mBinding.form.setLayoutManager(setupLayoutManager());
2170 mBinding.form.setAdapter(this);
2171 mBinding.actions.setAdapter(actionsAdapter);
2172 mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2173 if (execute(pos)) {
2174 removeSession(CommandSession.this);
2175 }
2176 });
2177
2178 actionsAdapter.notifyDataSetChanged();
2179 }
2180 }
2181 }
2182}