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 showViewPager() {
1184 pagerAdapter.show();
1185 }
1186
1187 public void hideViewPager() {
1188 pagerAdapter.hide();
1189 }
1190
1191 public interface OnMessageFound {
1192 void onMessageFound(final Message message);
1193 }
1194
1195 public static class Draft {
1196 private final String message;
1197 private final long timestamp;
1198
1199 private Draft(String message, long timestamp) {
1200 this.message = message;
1201 this.timestamp = timestamp;
1202 }
1203
1204 public long getTimestamp() {
1205 return timestamp;
1206 }
1207
1208 public String getMessage() {
1209 return message;
1210 }
1211 }
1212
1213 public class ConversationPagerAdapter extends PagerAdapter {
1214 protected ViewPager mPager = null;
1215 protected TabLayout mTabs = null;
1216 ArrayList<CommandSession> sessions = new ArrayList<>();
1217
1218 public void setupViewPager(ViewPager pager, TabLayout tabs) {
1219 mPager = pager;
1220 mTabs = tabs;
1221 show();
1222 pager.setAdapter(this);
1223 tabs.setupWithViewPager(mPager);
1224 pager.setCurrentItem(getCurrentTab());
1225
1226 mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1227 public void onPageScrollStateChanged(int state) { }
1228 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1229
1230 public void onPageSelected(int position) {
1231 setCurrentTab(position);
1232 }
1233 });
1234 }
1235
1236 public void show() {
1237 if (sessions == null) {
1238 sessions = new ArrayList<>();
1239 notifyDataSetChanged();
1240 }
1241 mTabs.setVisibility(View.VISIBLE);
1242 }
1243
1244 public void hide() {
1245 mPager.setCurrentItem(0);
1246 mTabs.setVisibility(View.GONE);
1247 sessions = null;
1248 notifyDataSetChanged();
1249 }
1250
1251 public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1252 CommandSession session = new CommandSession(command.getAttribute("name"), xmppConnectionService);
1253
1254 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1255 packet.setTo(command.getAttributeAsJid("jid"));
1256 final Element c = packet.addChild("command", Namespace.COMMANDS);
1257 c.setAttribute("node", command.getAttribute("node"));
1258 c.setAttribute("action", "execute");
1259 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1260 mPager.post(() -> {
1261 session.updateWithResponse(iq);
1262 });
1263 });
1264
1265 sessions.add(session);
1266 notifyDataSetChanged();
1267 mPager.setCurrentItem(getCount() - 1);
1268 }
1269
1270 public void removeSession(CommandSession session) {
1271 sessions.remove(session);
1272 notifyDataSetChanged();
1273 }
1274
1275 @NonNull
1276 @Override
1277 public Object instantiateItem(@NonNull ViewGroup container, int position) {
1278 if (position < 2) {
1279 return mPager.getChildAt(position);
1280 }
1281
1282 CommandSession session = sessions.get(position-2);
1283 CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
1284 container.addView(binding.getRoot());
1285 session.setBinding(binding);
1286 return session;
1287 }
1288
1289 @Override
1290 public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1291 if (position < 2) return;
1292
1293 container.removeView(((CommandSession) o).getView());
1294 }
1295
1296 @Override
1297 public int getItemPosition(Object o) {
1298 if (o == mPager.getChildAt(0)) return PagerAdapter.POSITION_UNCHANGED;
1299 if (o == mPager.getChildAt(1)) return PagerAdapter.POSITION_UNCHANGED;
1300
1301 int pos = sessions.indexOf(o);
1302 if (pos < 0) return PagerAdapter.POSITION_NONE;
1303 return pos + 2;
1304 }
1305
1306 @Override
1307 public int getCount() {
1308 if (sessions == null) return 1;
1309
1310 int count = 2 + sessions.size();
1311 if (count > 2) {
1312 mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1313 } else {
1314 mTabs.setTabMode(TabLayout.MODE_FIXED);
1315 }
1316 return count;
1317 }
1318
1319 @Override
1320 public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1321 if (view == o) return true;
1322
1323 if (o instanceof CommandSession) {
1324 return ((CommandSession) o).getView() == view;
1325 }
1326
1327 return false;
1328 }
1329
1330 @Nullable
1331 @Override
1332 public CharSequence getPageTitle(int position) {
1333 switch (position) {
1334 case 0:
1335 return "Conversation";
1336 case 1:
1337 return "Commands";
1338 default:
1339 CommandSession session = sessions.get(position-2);
1340 if (session == null) return super.getPageTitle(position);
1341 return session.getTitle();
1342 }
1343 }
1344
1345 class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
1346 abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1347 protected T binding;
1348
1349 public ViewHolder(T binding) {
1350 super(binding.getRoot());
1351 this.binding = binding;
1352 }
1353
1354 abstract public void bind(Item el);
1355
1356 protected void setTextOrHide(TextView v, Optional<String> s) {
1357 if (s == null || !s.isPresent()) {
1358 v.setVisibility(View.GONE);
1359 } else {
1360 v.setVisibility(View.VISIBLE);
1361 v.setText(s.get());
1362 }
1363 }
1364
1365 protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
1366 int flags = 0;
1367 if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
1368 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1369
1370 String type = field.getAttribute("type");
1371 if (type != null) {
1372 if (type.equals("text-multi") || type.equals("jid-multi")) {
1373 flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
1374 }
1375
1376 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1377
1378 if (type.equals("jid-single") || type.equals("jid-multi")) {
1379 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1380 }
1381
1382 if (type.equals("text-private")) {
1383 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1384 if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
1385 }
1386 }
1387
1388 Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1389 if (validate == null) return;
1390 String datatype = validate.getAttribute("datatype");
1391 if (datatype == null) return;
1392
1393 if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
1394 textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
1395 }
1396
1397 if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
1398 textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
1399 }
1400
1401 if (datatype.equals("xs:date")) {
1402 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
1403 }
1404
1405 if (datatype.equals("xs:dateTime")) {
1406 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
1407 }
1408
1409 if (datatype.equals("xs:time")) {
1410 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
1411 }
1412
1413 if (datatype.equals("xs:anyURI")) {
1414 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
1415 }
1416
1417 if (datatype.equals("html:tel")) {
1418 textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
1419 }
1420
1421 if (datatype.equals("html:email")) {
1422 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1423 }
1424 }
1425 }
1426
1427 class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1428 public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1429
1430 @Override
1431 public void bind(Item iq) {
1432 binding.errorIcon.setVisibility(View.VISIBLE);
1433
1434 Element error = iq.el.findChild("error");
1435 if (error == null) return;
1436 String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1437 if (text == null || text.equals("")) {
1438 text = error.getChildren().get(0).getName();
1439 }
1440 binding.message.setText(text);
1441 }
1442 }
1443
1444 class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1445 public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1446
1447 @Override
1448 public void bind(Item note) {
1449 binding.message.setText(note.el.getContent());
1450
1451 String type = note.el.getAttribute("type");
1452 if (type != null && type.equals("error")) {
1453 binding.errorIcon.setVisibility(View.VISIBLE);
1454 }
1455 }
1456 }
1457
1458 class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1459 public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1460
1461 @Override
1462 public void bind(Item item) {
1463 Field field = (Field) item;
1464 setTextOrHide(binding.label, field.getLabel());
1465 setTextOrHide(binding.desc, field.getDesc());
1466
1467 ArrayAdapter<String> values = new ArrayAdapter<String>(binding.getRoot().getContext(), R.layout.simple_list_item);
1468 for (Element el : field.el.getChildren()) {
1469 if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1470 values.add(el.getContent());
1471 }
1472 }
1473 binding.values.setAdapter(values);
1474
1475 ClipboardManager clipboard = binding.getRoot().getContext().getSystemService(ClipboardManager.class);
1476 binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
1477 ClipData myClip = ClipData.newPlainText("text", values.getItem(pos));
1478 clipboard.setPrimaryClip(myClip);
1479 Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1480 return true;
1481 });
1482 }
1483 }
1484
1485 class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
1486 public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
1487
1488 @Override
1489 public void bind(Item item) {
1490 Cell cell = (Cell) item;
1491
1492 if (cell.el == null) {
1493 binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
1494 setTextOrHide(binding.text, cell.reported.getLabel());
1495 } else {
1496 binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
1497 binding.text.setText(cell.el.findChildContent("value", "jabber:x:data"));
1498 }
1499 }
1500 }
1501
1502 class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
1503 public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
1504 super(binding);
1505 binding.row.setOnClickListener((v) -> {
1506 binding.checkbox.toggle();
1507 });
1508 binding.checkbox.setOnCheckedChangeListener(this);
1509 }
1510 protected Element mValue = null;
1511
1512 @Override
1513 public void bind(Item item) {
1514 Field field = (Field) item;
1515 binding.label.setText(field.getLabel().orElse(""));
1516 setTextOrHide(binding.desc, field.getDesc());
1517 mValue = field.getValue();
1518 binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
1519 }
1520
1521 @Override
1522 public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
1523 if (mValue == null) return;
1524
1525 mValue.setContent(isChecked ? "true" : "false");
1526 }
1527 }
1528
1529 class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
1530 public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
1531 super(binding);
1532 binding.open.addTextChangedListener(this);
1533 options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
1534 @Override
1535 public View getView(int position, View convertView, ViewGroup parent) {
1536 CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
1537 v.setId(position);
1538 v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
1539 v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
1540 return v;
1541 }
1542 };
1543 }
1544 protected Element mValue = null;
1545 protected ArrayAdapter<Option> options;
1546
1547 @Override
1548 public void bind(Item item) {
1549 Field field = (Field) item;
1550 setTextOrHide(binding.label, field.getLabel());
1551 setTextOrHide(binding.desc, field.getDesc());
1552
1553 if (field.error != null) {
1554 binding.desc.setVisibility(View.VISIBLE);
1555 binding.desc.setText(field.error);
1556 binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Design_Error);
1557 } else {
1558 binding.desc.setTextAppearance(R.style.TextAppearance_Conversations_Status);
1559 }
1560
1561 mValue = field.getValue();
1562
1563 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1564 binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
1565 binding.open.setText(mValue.getContent());
1566 setupInputType(field.el, binding.open, null);
1567
1568 options.clear();
1569 List<Option> theOptions = field.getOptions();
1570 options.addAll(theOptions);
1571
1572 float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
1573 TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
1574 float maxColumnWidth = theOptions.stream().map((x) ->
1575 StaticLayout.getDesiredWidth(x.toString(), paint)
1576 ).max(Float::compare).orElse(new Float(0.0));
1577 if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
1578 binding.radios.setNumColumns(theOptions.size());
1579 } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
1580 binding.radios.setNumColumns(theOptions.size() / 2);
1581 } else {
1582 binding.radios.setNumColumns(1);
1583 }
1584 binding.radios.setAdapter(options);
1585 }
1586
1587 @Override
1588 public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
1589 if (mValue == null) return;
1590
1591 if (isChecked) {
1592 mValue.setContent(options.getItem(radio.getId()).getValue());
1593 binding.open.setText(mValue.getContent());
1594 }
1595 options.notifyDataSetChanged();
1596 }
1597
1598 @Override
1599 public void afterTextChanged(Editable s) {
1600 if (mValue == null) return;
1601
1602 mValue.setContent(s.toString());
1603 options.notifyDataSetChanged();
1604 }
1605
1606 @Override
1607 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1608
1609 @Override
1610 public void onTextChanged(CharSequence s, int start, int count, int after) { }
1611 }
1612
1613 class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
1614 public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
1615 super(binding);
1616 binding.spinner.setOnItemSelectedListener(this);
1617 }
1618 protected Element mValue = null;
1619
1620 @Override
1621 public void bind(Item item) {
1622 Field field = (Field) item;
1623 setTextOrHide(binding.label, field.getLabel());
1624 binding.spinner.setPrompt(field.getLabel().orElse(""));
1625 setTextOrHide(binding.desc, field.getDesc());
1626
1627 mValue = field.getValue();
1628
1629 ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
1630 options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1631 options.addAll(field.getOptions());
1632
1633 binding.spinner.setAdapter(options);
1634 binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
1635 }
1636
1637 @Override
1638 public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
1639 Option o = (Option) parent.getItemAtPosition(pos);
1640 if (mValue == null) return;
1641
1642 mValue.setContent(o == null ? "" : o.getValue());
1643 }
1644
1645 @Override
1646 public void onNothingSelected(AdapterView<?> parent) {
1647 mValue.setContent("");
1648 }
1649 }
1650
1651 class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
1652 public TextFieldViewHolder(CommandTextFieldBinding binding) {
1653 super(binding);
1654 binding.textinput.addTextChangedListener(this);
1655 }
1656 protected Element mValue = null;
1657
1658 @Override
1659 public void bind(Item item) {
1660 Field field = (Field) item;
1661 binding.textinputLayout.setHint(field.getLabel().orElse(""));
1662
1663 binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
1664 field.getDesc().ifPresent(binding.textinputLayout::setHelperText);
1665
1666 binding.textinputLayout.setErrorEnabled(field.error != null);
1667 if (field.error != null) binding.textinputLayout.setError(field.error);
1668
1669 mValue = field.getValue();
1670 binding.textinput.setText(mValue.getContent());
1671 setupInputType(field.el, binding.textinput, binding.textinputLayout);
1672 }
1673
1674 @Override
1675 public void afterTextChanged(Editable s) {
1676 if (mValue == null) return;
1677
1678 mValue.setContent(s.toString());
1679 }
1680
1681 @Override
1682 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1683
1684 @Override
1685 public void onTextChanged(CharSequence s, int start, int count, int after) { }
1686 }
1687
1688 class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
1689 public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
1690
1691 @Override
1692 public void bind(Item oob) {
1693 binding.webview.getSettings().setJavaScriptEnabled(true);
1694 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");
1695 binding.webview.getSettings().setDatabaseEnabled(true);
1696 binding.webview.getSettings().setDomStorageEnabled(true);
1697 binding.webview.setWebViewClient(new WebViewClient() {
1698 @Override
1699 public void onPageFinished(WebView view, String url) {
1700 super.onPageFinished(view, url);
1701 mTitle = view.getTitle();
1702 ConversationPagerAdapter.this.notifyDataSetChanged();
1703 }
1704 });
1705 binding.webview.loadUrl(oob.el.findChildContent("url", "jabber:x:oob"));
1706 }
1707 }
1708
1709 class Item {
1710 protected Element el;
1711 protected int viewType;
1712 protected String error = null;
1713
1714 Item(Element el, int viewType) {
1715 this.el = el;
1716 this.viewType = viewType;
1717 }
1718
1719 public boolean validate() {
1720 error = null;
1721 return true;
1722 }
1723 }
1724
1725 class Field extends Item {
1726 Field(Element el, int viewType) { super(el, viewType); }
1727
1728 @Override
1729 public boolean validate() {
1730 if (!super.validate()) return false;
1731 if (el.findChild("required", "jabber:x:data") == null) return true;
1732 if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
1733
1734 error = "this value is required";
1735 return false;
1736 }
1737
1738 public String getVar() {
1739 return el.getAttribute("var");
1740 }
1741
1742 public Optional<String> getLabel() {
1743 String label = el.getAttribute("label");
1744 if (label == null) label = getVar();
1745 return Optional.ofNullable(label);
1746 }
1747
1748 public Optional<String> getDesc() {
1749 return Optional.ofNullable(el.findChildContent("desc", "jabber:x:data"));
1750 }
1751
1752 public Element getValue() {
1753 Element value = el.findChild("value", "jabber:x:data");
1754 if (value == null) {
1755 value = el.addChild("value", "jabber:x:data");
1756 }
1757 return value;
1758 }
1759
1760 public List<Option> getOptions() {
1761 return Option.forField(el);
1762 }
1763 }
1764
1765 class Cell extends Item {
1766 protected Field reported;
1767
1768 Cell(Field reported, Element item) {
1769 super(item, TYPE_RESULT_CELL);
1770 this.reported = reported;
1771 }
1772 }
1773
1774 protected Field mkField(Element el) {
1775 int viewType = -1;
1776
1777 String formType = responseElement.getAttribute("type");
1778 if (formType != null) {
1779 String fieldType = el.getAttribute("type");
1780 if (fieldType == null) fieldType = "text-single";
1781
1782 if (formType.equals("result") || fieldType.equals("fixed")) {
1783 viewType = TYPE_RESULT_FIELD;
1784 } else if (formType.equals("form")) {
1785 if (fieldType.equals("boolean")) {
1786 viewType = TYPE_CHECKBOX_FIELD;
1787 } else if (fieldType.equals("list-single")) {
1788 Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1789 if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
1790 viewType = TYPE_RADIO_EDIT_FIELD;
1791 } else {
1792 viewType = TYPE_SPINNER_FIELD;
1793 }
1794 } else {
1795 viewType = TYPE_TEXT_FIELD;
1796 }
1797 }
1798
1799 return new Field(el, viewType);
1800 }
1801
1802 return null;
1803 }
1804
1805 protected Item mkItem(Element el, int pos) {
1806 int viewType = -1;
1807
1808 if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
1809 if (el.getName().equals("note")) {
1810 viewType = TYPE_NOTE;
1811 } else if (el.getNamespace().equals("jabber:x:oob")) {
1812 viewType = TYPE_WEB;
1813 } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
1814 viewType = TYPE_NOTE;
1815 } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
1816 Field field = mkField(el);
1817 if (field != null) {
1818 items.put(pos, field);
1819 return field;
1820 }
1821 }
1822 } else if (response != null) {
1823 viewType = TYPE_ERROR;
1824 }
1825
1826 Item item = new Item(el, viewType);
1827 items.put(pos, item);
1828 return item;
1829 }
1830
1831 final int TYPE_ERROR = 1;
1832 final int TYPE_NOTE = 2;
1833 final int TYPE_WEB = 3;
1834 final int TYPE_RESULT_FIELD = 4;
1835 final int TYPE_TEXT_FIELD = 5;
1836 final int TYPE_CHECKBOX_FIELD = 6;
1837 final int TYPE_SPINNER_FIELD = 7;
1838 final int TYPE_RADIO_EDIT_FIELD = 8;
1839 final int TYPE_RESULT_CELL = 9;
1840
1841 protected String mTitle;
1842 protected CommandPageBinding mBinding = null;
1843 protected IqPacket response = null;
1844 protected Element responseElement = null;
1845 protected List<Field> reported = null;
1846 protected SparseArray<Item> items = new SparseArray<>();
1847 protected XmppConnectionService xmppConnectionService;
1848 protected ArrayAdapter<String> actionsAdapter;
1849 protected GridLayoutManager layoutManager;
1850
1851 CommandSession(String title, XmppConnectionService xmppConnectionService) {
1852 mTitle = title;
1853 this.xmppConnectionService = xmppConnectionService;
1854 setupLayoutManager();
1855 actionsAdapter = new ArrayAdapter<String>(xmppConnectionService, R.layout.simple_list_item) {
1856 @Override
1857 public View getView(int position, View convertView, ViewGroup parent) {
1858 View v = super.getView(position, convertView, parent);
1859 TextView tv = (TextView) v.findViewById(android.R.id.text1);
1860 tv.setGravity(Gravity.CENTER);
1861 int resId = xmppConnectionService.getResources().getIdentifier("action_" + tv.getText() , "string" , xmppConnectionService.getPackageName());
1862 if (resId != 0) tv.setText(xmppConnectionService.getResources().getString(resId));
1863 tv.setTextColor(ContextCompat.getColor(xmppConnectionService, R.color.white));
1864 tv.setBackgroundColor(UIHelper.getColorForName(tv.getText().toString()));
1865 return v;
1866 }
1867 };
1868 actionsAdapter.registerDataSetObserver(new DataSetObserver() {
1869 @Override
1870 public void onChanged() {
1871 if (mBinding == null) return;
1872
1873 mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
1874 }
1875
1876 @Override
1877 public void onInvalidated() {}
1878 });
1879 }
1880
1881 public String getTitle() {
1882 return mTitle;
1883 }
1884
1885 public void updateWithResponse(IqPacket iq) {
1886 this.responseElement = null;
1887 this.reported = null;
1888 this.response = iq;
1889 this.items.clear();
1890 this.actionsAdapter.clear();
1891 layoutManager.setSpanCount(1);
1892
1893 Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1894 if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1895 for (Element el : command.getChildren()) {
1896 if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1897 for (Element action : el.getChildren()) {
1898 if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
1899 if (action.getName().equals("execute")) continue;
1900
1901 actionsAdapter.add(action.getName());
1902 }
1903 }
1904 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1905 String title = el.findChildContent("title", "jabber:x:data");
1906 if (title != null) {
1907 mTitle = title;
1908 ConversationPagerAdapter.this.notifyDataSetChanged();
1909 }
1910
1911 if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
1912 this.responseElement = el;
1913 setupReported(el.findChild("reported", "jabber:x:data"));
1914 layoutManager.setSpanCount(this.reported == null ? 1 : this.reported.size());
1915 }
1916 break;
1917 }
1918 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1919 String url = el.findChildContent("url", "jabber:x:oob");
1920 if (url != null) {
1921 String scheme = Uri.parse(url).getScheme();
1922 if (scheme.equals("http") || scheme.equals("https")) {
1923 this.responseElement = el;
1924 break;
1925 }
1926 }
1927 }
1928 if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1929 this.responseElement = el;
1930 break;
1931 }
1932 }
1933
1934 if (responseElement == null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
1935 removeSession(this);
1936 return;
1937 }
1938
1939 if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
1940 // No actions have been given, but we are not done?
1941 // This is probably a spec violation, but we should do *something*
1942 actionsAdapter.add("execute");
1943 }
1944 }
1945
1946 if (actionsAdapter.getCount() > 0) {
1947 if (actionsAdapter.getPosition("cancel") < 0) actionsAdapter.insert("cancel", 0);
1948 } else {
1949 actionsAdapter.add("close");
1950 }
1951
1952 notifyDataSetChanged();
1953 }
1954
1955 protected void setupReported(Element el) {
1956 if (el == null) {
1957 reported = null;
1958 return;
1959 }
1960
1961 reported = new ArrayList<>();
1962 for (Element fieldEl : el.getChildren()) {
1963 if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
1964 reported.add(mkField(fieldEl));
1965 }
1966 }
1967
1968 @Override
1969 public int getItemCount() {
1970 if (response == null) return 0;
1971 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
1972 int i = 0;
1973 for (Element el : responseElement.getChildren()) {
1974 if (!el.getNamespace().equals("jabber:x:data")) continue;
1975 if (el.getName().equals("title")) continue;
1976 if (el.getName().equals("field")) {
1977 String type = el.getAttribute("type");
1978 if (type != null && type.equals("hidden")) continue;
1979 }
1980
1981 if (el.getName().equals("reported") || el.getName().equals("item")) {
1982 if (reported != null) i += reported.size();
1983 continue;
1984 }
1985
1986 i++;
1987 }
1988 return i;
1989 }
1990 return 1;
1991 }
1992
1993 public Item getItem(int position) {
1994 if (items.get(position) != null) return items.get(position);
1995 if (response == null) return null;
1996
1997 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
1998 if (responseElement.getNamespace().equals("jabber:x:data")) {
1999 int i = 0;
2000 for (Element el : responseElement.getChildren()) {
2001 if (!el.getNamespace().equals("jabber:x:data")) continue;
2002 if (el.getName().equals("title")) continue;
2003 if (el.getName().equals("field")) {
2004 String type = el.getAttribute("type");
2005 if (type != null && type.equals("hidden")) continue;
2006 }
2007
2008 if (el.getName().equals("reported") || el.getName().equals("item")) {
2009 Cell cell = null;
2010
2011 if (reported != null) {
2012 if (reported.size() > position - i) {
2013 Field reportedField = reported.get(position - i);
2014 Element itemField = null;
2015 if (el.getName().equals("item")) {
2016 for (Element subel : el.getChildren()) {
2017 if (subel.getAttribute("var").equals(reportedField.getVar())) {
2018 itemField = subel;
2019 break;
2020 }
2021 }
2022 }
2023 cell = new Cell(reportedField, itemField);
2024 } else {
2025 i += reported.size();
2026 continue;
2027 }
2028 }
2029
2030 if (cell != null) {
2031 items.put(position, cell);
2032 return cell;
2033 }
2034 }
2035
2036 if (i < position) {
2037 i++;
2038 continue;
2039 }
2040
2041 return mkItem(el, position);
2042 }
2043 }
2044 }
2045
2046 return mkItem(responseElement == null ? response : responseElement, position);
2047 }
2048
2049 @Override
2050 public int getItemViewType(int position) {
2051 return getItem(position).viewType;
2052 }
2053
2054 @Override
2055 public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2056 switch(viewType) {
2057 case TYPE_ERROR: {
2058 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2059 return new ErrorViewHolder(binding);
2060 }
2061 case TYPE_NOTE: {
2062 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2063 return new NoteViewHolder(binding);
2064 }
2065 case TYPE_WEB: {
2066 CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2067 return new WebViewHolder(binding);
2068 }
2069 case TYPE_RESULT_FIELD: {
2070 CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2071 return new ResultFieldViewHolder(binding);
2072 }
2073 case TYPE_RESULT_CELL: {
2074 CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2075 return new ResultCellViewHolder(binding);
2076 }
2077 case TYPE_CHECKBOX_FIELD: {
2078 CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2079 return new CheckboxFieldViewHolder(binding);
2080 }
2081 case TYPE_RADIO_EDIT_FIELD: {
2082 CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2083 return new RadioEditFieldViewHolder(binding);
2084 }
2085 case TYPE_SPINNER_FIELD: {
2086 CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2087 return new SpinnerFieldViewHolder(binding);
2088 }
2089 case TYPE_TEXT_FIELD: {
2090 CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2091 return new TextFieldViewHolder(binding);
2092 }
2093 default:
2094 throw new IllegalArgumentException("Unknown viewType: " + viewType);
2095 }
2096 }
2097
2098 @Override
2099 public void onBindViewHolder(ViewHolder viewHolder, int position) {
2100 viewHolder.bind(getItem(position));
2101 }
2102
2103 public View getView() {
2104 return mBinding.getRoot();
2105 }
2106
2107 public boolean validate() {
2108 int count = getItemCount();
2109 boolean isValid = true;
2110 for (int i = 0; i < count; i++) {
2111 boolean oneIsValid = getItem(i).validate();
2112 isValid = isValid && oneIsValid;
2113 }
2114 notifyDataSetChanged();
2115 return isValid;
2116 }
2117
2118 public boolean execute() {
2119 return execute("execute");
2120 }
2121
2122 public boolean execute(int actionPosition) {
2123 return execute(actionsAdapter.getItem(actionPosition));
2124 }
2125
2126 public boolean execute(String action) {
2127 if (!action.equals("cancel") && !validate()) return false;
2128 if (response == null) return true;
2129 Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2130 if (command == null) return true;
2131 String status = command.getAttribute("status");
2132 if (status == null || !status.equals("executing")) return true;
2133
2134 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2135 packet.setTo(response.getFrom());
2136 final Element c = packet.addChild("command", Namespace.COMMANDS);
2137 c.setAttribute("node", command.getAttribute("node"));
2138 c.setAttribute("sessionid", command.getAttribute("sessionid"));
2139 c.setAttribute("action", action);
2140
2141 String formType = responseElement == null ? null : responseElement.getAttribute("type");
2142 if (!action.equals("cancel") &&
2143 responseElement != null &&
2144 responseElement.getName().equals("x") &&
2145 responseElement.getNamespace().equals("jabber:x:data") &&
2146 formType != null && formType.equals("form")) {
2147
2148 responseElement.setAttribute("type", "submit");
2149 c.addChild(responseElement);
2150 }
2151
2152 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2153 getView().post(() -> {
2154 updateWithResponse(iq);
2155 });
2156 });
2157
2158 return false;
2159 }
2160
2161 protected GridLayoutManager setupLayoutManager() {
2162 layoutManager = new GridLayoutManager(mPager.getContext(), layoutManager == null ? 1 : layoutManager.getSpanCount()) {
2163 @Override
2164 public boolean canScrollVertically() { return getItemCount() > 1; }
2165 };
2166 layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2167 @Override
2168 public int getSpanSize(int position) {
2169 if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2170 return 1;
2171 }
2172 });
2173 return layoutManager;
2174 }
2175
2176 public void setBinding(CommandPageBinding b) {
2177 mBinding = b;
2178 mBinding.form.setLayoutManager(setupLayoutManager());
2179 mBinding.form.setAdapter(this);
2180 mBinding.actions.setAdapter(actionsAdapter);
2181 mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2182 if (execute(pos)) {
2183 removeSession(CommandSession.this);
2184 }
2185 });
2186
2187 actionsAdapter.notifyDataSetChanged();
2188 }
2189 }
2190 }
2191}