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