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 mValue = field.getValue();
1545
1546 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1547 binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
1548 binding.open.setText(mValue.getContent());
1549 setupInputType(field.el, binding.open, null);
1550
1551 options.clear();
1552 List<Option> theOptions = field.getOptions();
1553 options.addAll(theOptions);
1554
1555 float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
1556 TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
1557 float maxColumnWidth = theOptions.stream().map((x) ->
1558 StaticLayout.getDesiredWidth(x.toString(), paint)
1559 ).max(Float::compare).orElse(new Float(0.0));
1560 if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
1561 binding.radios.setNumColumns(theOptions.size());
1562 } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
1563 binding.radios.setNumColumns(theOptions.size() / 2);
1564 } else {
1565 binding.radios.setNumColumns(1);
1566 }
1567 binding.radios.setAdapter(options);
1568 }
1569
1570 @Override
1571 public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
1572 if (mValue == null) return;
1573
1574 if (isChecked) {
1575 mValue.setContent(options.getItem(radio.getId()).getValue());
1576 binding.open.setText(mValue.getContent());
1577 }
1578 options.notifyDataSetChanged();
1579 }
1580
1581 @Override
1582 public void afterTextChanged(Editable s) {
1583 if (mValue == null) return;
1584
1585 mValue.setContent(s.toString());
1586 options.notifyDataSetChanged();
1587 }
1588
1589 @Override
1590 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1591
1592 @Override
1593 public void onTextChanged(CharSequence s, int start, int count, int after) { }
1594 }
1595
1596 class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
1597 public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
1598 super(binding);
1599 binding.spinner.setOnItemSelectedListener(this);
1600 }
1601 protected Element mValue = null;
1602
1603 @Override
1604 public void bind(Item item) {
1605 Field field = (Field) item;
1606 setTextOrHide(binding.label, field.getLabel());
1607 binding.spinner.setPrompt(field.getLabel().orElse(""));
1608 setTextOrHide(binding.desc, field.getDesc());
1609
1610 mValue = field.getValue();
1611
1612 ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
1613 options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
1614 options.addAll(field.getOptions());
1615
1616 binding.spinner.setAdapter(options);
1617 binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
1618 }
1619
1620 @Override
1621 public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
1622 Option o = (Option) parent.getItemAtPosition(pos);
1623 if (mValue == null) return;
1624
1625 mValue.setContent(o == null ? "" : o.getValue());
1626 }
1627
1628 @Override
1629 public void onNothingSelected(AdapterView<?> parent) {
1630 mValue.setContent("");
1631 }
1632 }
1633
1634 class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
1635 public TextFieldViewHolder(CommandTextFieldBinding binding) {
1636 super(binding);
1637 binding.textinput.addTextChangedListener(this);
1638 }
1639 protected Element mValue = null;
1640
1641 @Override
1642 public void bind(Item item) {
1643 Field field = (Field) item;
1644 binding.textinputLayout.setHint(field.getLabel().orElse(""));
1645
1646 binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
1647 field.getDesc().ifPresent(binding.textinputLayout::setHelperText);
1648
1649 mValue = field.getValue();
1650 binding.textinput.setText(mValue.getContent());
1651 setupInputType(field.el, binding.textinput, binding.textinputLayout);
1652 }
1653
1654 @Override
1655 public void afterTextChanged(Editable s) {
1656 if (mValue == null) return;
1657
1658 mValue.setContent(s.toString());
1659 }
1660
1661 @Override
1662 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1663
1664 @Override
1665 public void onTextChanged(CharSequence s, int start, int count, int after) { }
1666 }
1667
1668 class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
1669 public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
1670
1671 @Override
1672 public void bind(Item oob) {
1673 binding.webview.getSettings().setJavaScriptEnabled(true);
1674 binding.webview.setWebViewClient(new WebViewClient() {
1675 @Override
1676 public void onPageFinished(WebView view, String url) {
1677 super.onPageFinished(view, url);
1678 mTitle = view.getTitle();
1679 ConversationPagerAdapter.this.notifyDataSetChanged();
1680 }
1681 });
1682 binding.webview.loadUrl(oob.el.findChildContent("url", "jabber:x:oob"));
1683 }
1684 }
1685
1686 class Item {
1687 protected Element el;
1688 protected int viewType;
1689
1690 Item(Element el, int viewType) {
1691 this.el = el;
1692 this.viewType = viewType;
1693 }
1694 }
1695
1696 class Field extends Item {
1697 Field(Element el, int viewType) { super(el, viewType); }
1698
1699 public Optional<String> getLabel() {
1700 String label = el.getAttribute("label");
1701 if (label == null) label = el.getAttribute("var");
1702 return Optional.ofNullable(label);
1703 }
1704
1705 public Optional<String> getDesc() {
1706 return Optional.ofNullable(el.findChildContent("desc", "jabber:x:data"));
1707 }
1708
1709 public Element getValue() {
1710 Element value = el.findChild("value", "jabber:x:data");
1711 if (value == null) {
1712 value = el.addChild("value", "jabber:x:data");
1713 }
1714 return value;
1715 }
1716
1717 public List<Option> getOptions() {
1718 return Option.forField(el);
1719 }
1720 }
1721
1722 class Cell extends Item {
1723 protected Element reported;
1724
1725 Cell(Element reported, Element item) {
1726 super(item, TYPE_RESULT_CELL);
1727 this.reported = reported;
1728 }
1729 }
1730
1731 protected Item mkItem(Element el, int pos) {
1732 int viewType = -1;
1733
1734 if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
1735 if (el.getName().equals("note")) {
1736 viewType = TYPE_NOTE;
1737 } else if (el.getNamespace().equals("jabber:x:oob")) {
1738 viewType = TYPE_WEB;
1739 } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
1740 viewType = TYPE_NOTE;
1741 } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
1742 String formType = responseElement.getAttribute("type");
1743 if (formType != null) {
1744 String fieldType = el.getAttribute("type");
1745 if (fieldType == null) fieldType = "text-single";
1746
1747 if (formType.equals("result") || fieldType.equals("fixed")) {
1748 viewType = TYPE_RESULT_FIELD;
1749 } else if (formType.equals("form")) {
1750 if (fieldType.equals("boolean")) {
1751 viewType = TYPE_CHECKBOX_FIELD;
1752 } else if (fieldType.equals("list-single")) {
1753 Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1754 if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
1755 viewType = TYPE_RADIO_EDIT_FIELD;
1756 } else {
1757 viewType = TYPE_SPINNER_FIELD;
1758 }
1759 } else {
1760 viewType = TYPE_TEXT_FIELD;
1761 }
1762 }
1763
1764 Field field = new Field(el, viewType);
1765 items.put(pos, field);
1766 return field;
1767 }
1768 }
1769 } else if (response != null) {
1770 viewType = TYPE_ERROR;
1771 }
1772
1773 Item item = new Item(el, viewType);
1774 items.put(pos, item);
1775 return item;
1776 }
1777
1778 final int TYPE_ERROR = 1;
1779 final int TYPE_NOTE = 2;
1780 final int TYPE_WEB = 3;
1781 final int TYPE_RESULT_FIELD = 4;
1782 final int TYPE_TEXT_FIELD = 5;
1783 final int TYPE_CHECKBOX_FIELD = 6;
1784 final int TYPE_SPINNER_FIELD = 7;
1785 final int TYPE_RADIO_EDIT_FIELD = 8;
1786 final int TYPE_RESULT_CELL = 9;
1787
1788 protected String mTitle;
1789 protected CommandPageBinding mBinding = null;
1790 protected IqPacket response = null;
1791 protected Element responseElement = null;
1792 protected Element reported = null;
1793 protected SparseArray<Item> items = new SparseArray<>();
1794 protected XmppConnectionService xmppConnectionService;
1795 protected ArrayAdapter<String> actionsAdapter;
1796 protected GridLayoutManager layoutManager;
1797
1798 CommandSession(String title, XmppConnectionService xmppConnectionService) {
1799 mTitle = title;
1800 this.xmppConnectionService = xmppConnectionService;
1801 setupLayoutManager();
1802 actionsAdapter = new ArrayAdapter<String>(xmppConnectionService, R.layout.simple_list_item) {
1803 @Override
1804 public View getView(int position, View convertView, ViewGroup parent) {
1805 View v = super.getView(position, convertView, parent);
1806 TextView tv = (TextView) v.findViewById(android.R.id.text1);
1807 tv.setGravity(Gravity.CENTER);
1808 int resId = xmppConnectionService.getResources().getIdentifier("action_" + tv.getText() , "string" , xmppConnectionService.getPackageName());
1809 if (resId != 0) tv.setText(xmppConnectionService.getResources().getString(resId));
1810 tv.setTextColor(ContextCompat.getColor(xmppConnectionService, R.color.white));
1811 tv.setBackgroundColor(UIHelper.getColorForName(tv.getText().toString()));
1812 return v;
1813 }
1814 };
1815 actionsAdapter.registerDataSetObserver(new DataSetObserver() {
1816 @Override
1817 public void onChanged() {
1818 if (mBinding == null) return;
1819
1820 mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
1821 }
1822
1823 @Override
1824 public void onInvalidated() {}
1825 });
1826 }
1827
1828 public String getTitle() {
1829 return mTitle;
1830 }
1831
1832 public void updateWithResponse(IqPacket iq) {
1833 this.responseElement = null;
1834 this.reported = null;
1835 this.response = iq;
1836 this.items.clear();
1837 this.actionsAdapter.clear();
1838 layoutManager.setSpanCount(1);
1839
1840 Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
1841 if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
1842 for (Element el : command.getChildren()) {
1843 if (el.getName().equals("actions") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1844 for (Element action : el.getChildren()) {
1845 if (!el.getNamespace().equals("http://jabber.org/protocol/commands")) continue;
1846 if (action.getName().equals("execute")) continue;
1847
1848 actionsAdapter.add(action.getName());
1849 }
1850 }
1851 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:data")) {
1852 String title = el.findChildContent("title", "jabber:x:data");
1853 if (title != null) {
1854 mTitle = title;
1855 ConversationPagerAdapter.this.notifyDataSetChanged();
1856 }
1857
1858 if (el.getAttribute("type").equals("result") || el.getAttribute("type").equals("form")) {
1859 this.responseElement = el;
1860 this.reported = el.findChild("reported", "jabber:x:data");
1861 layoutManager.setSpanCount(this.reported == null ? 1 : this.reported.getChildren().size());
1862 }
1863 break;
1864 }
1865 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
1866 String url = el.findChildContent("url", "jabber:x:oob");
1867 if (url != null) {
1868 String scheme = Uri.parse(url).getScheme();
1869 if (scheme.equals("http") || scheme.equals("https")) {
1870 this.responseElement = el;
1871 break;
1872 }
1873 }
1874 }
1875 if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
1876 this.responseElement = el;
1877 break;
1878 }
1879 }
1880
1881 if (responseElement == null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
1882 removeSession(this);
1883 return;
1884 }
1885
1886 if (command.getAttribute("status").equals("executing") && actionsAdapter.getCount() < 1) {
1887 // No actions have been given, but we are not done?
1888 // This is probably a spec violation, but we should do *something*
1889 actionsAdapter.add("execute");
1890 }
1891 }
1892
1893 if (actionsAdapter.getCount() > 0) {
1894 if (actionsAdapter.getPosition("cancel") < 0) actionsAdapter.insert("cancel", 0);
1895 } else {
1896 actionsAdapter.add("close");
1897 }
1898
1899 notifyDataSetChanged();
1900 }
1901
1902 @Override
1903 public int getItemCount() {
1904 if (response == null) return 0;
1905 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
1906 int i = 0;
1907 for (Element el : responseElement.getChildren()) {
1908 if (!el.getNamespace().equals("jabber:x:data")) continue;
1909 if (el.getName().equals("title")) continue;
1910 if (el.getName().equals("field")) {
1911 String type = el.getAttribute("type");
1912 if (type != null && type.equals("hidden")) continue;
1913 }
1914
1915 if (el.getName().equals("reported") || el.getName().equals("item")) {
1916 i += el.getChildren().size();
1917 continue;
1918 }
1919
1920 i++;
1921 }
1922 return i;
1923 }
1924 return 1;
1925 }
1926
1927 public Item getItem(int position) {
1928 if (items.get(position) != null) return items.get(position);
1929 if (response == null) return null;
1930
1931 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
1932 if (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 int col = 0;
1944 for (Element subel : el.getChildren()) {
1945 if (i < position) {
1946 i++;
1947 col++;
1948 continue;
1949 }
1950
1951 Element reportedField = null;
1952 if (reported != null) {
1953 int rCol = 0;
1954 for (Element field : reported.getChildren()) {
1955 if (!field.getName().equals("field") || !field.getNamespace().equals("jabber:x:data")) continue;
1956 if (rCol < col) {
1957 rCol++;
1958 continue;
1959 }
1960 reportedField = field;
1961 break;
1962 }
1963 }
1964 Cell cell = new Cell(reportedField, el.getName().equals("item") ? subel : null);
1965 items.put(position, cell);
1966 return cell;
1967 }
1968
1969 i--;
1970 }
1971
1972 if (i < position) {
1973 i++;
1974 continue;
1975 }
1976
1977 return mkItem(el, position);
1978 }
1979 }
1980 }
1981
1982 return mkItem(responseElement == null ? response : responseElement, position);
1983 }
1984
1985 @Override
1986 public int getItemViewType(int position) {
1987 return getItem(position).viewType;
1988 }
1989
1990 @Override
1991 public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
1992 switch(viewType) {
1993 case TYPE_ERROR: {
1994 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1995 return new ErrorViewHolder(binding);
1996 }
1997 case TYPE_NOTE: {
1998 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
1999 return new NoteViewHolder(binding);
2000 }
2001 case TYPE_WEB: {
2002 CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2003 return new WebViewHolder(binding);
2004 }
2005 case TYPE_RESULT_FIELD: {
2006 CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2007 return new ResultFieldViewHolder(binding);
2008 }
2009 case TYPE_RESULT_CELL: {
2010 CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2011 return new ResultCellViewHolder(binding);
2012 }
2013 case TYPE_CHECKBOX_FIELD: {
2014 CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2015 return new CheckboxFieldViewHolder(binding);
2016 }
2017 case TYPE_RADIO_EDIT_FIELD: {
2018 CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2019 return new RadioEditFieldViewHolder(binding);
2020 }
2021 case TYPE_SPINNER_FIELD: {
2022 CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2023 return new SpinnerFieldViewHolder(binding);
2024 }
2025 case TYPE_TEXT_FIELD: {
2026 CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2027 return new TextFieldViewHolder(binding);
2028 }
2029 default:
2030 throw new IllegalArgumentException("Unknown viewType: " + viewType);
2031 }
2032 }
2033
2034 @Override
2035 public void onBindViewHolder(ViewHolder viewHolder, int position) {
2036 viewHolder.bind(getItem(position));
2037 }
2038
2039 public View getView() {
2040 return mBinding.getRoot();
2041 }
2042
2043 public boolean execute() {
2044 return execute("execute");
2045 }
2046
2047 public boolean execute(int actionPosition) {
2048 return execute(actionsAdapter.getItem(actionPosition));
2049 }
2050
2051 public boolean execute(String action) {
2052 if (response == null || responseElement == null) return true;
2053 Element command = response.findChild("command", "http://jabber.org/protocol/commands");
2054 if (command == null) return true;
2055 String status = command.getAttribute("status");
2056 if (status == null || !status.equals("executing")) return true;
2057 if (!responseElement.getName().equals("x") || !responseElement.getNamespace().equals("jabber:x:data")) return true;
2058 String formType = responseElement.getAttribute("type");
2059 if (formType == null || !formType.equals("form")) return true;
2060
2061 responseElement.setAttribute("type", "submit");
2062
2063 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
2064 packet.setTo(response.getFrom());
2065 final Element c = packet.addChild("command", Namespace.COMMANDS);
2066 c.setAttribute("node", command.getAttribute("node"));
2067 c.setAttribute("sessionid", command.getAttribute("sessionid"));
2068 c.setAttribute("action", action);
2069 c.addChild(responseElement);
2070
2071 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
2072 getView().post(() -> {
2073 updateWithResponse(iq);
2074 });
2075 });
2076
2077 return false;
2078 }
2079
2080 protected GridLayoutManager setupLayoutManager() {
2081 layoutManager = new GridLayoutManager(mPager.getContext(), layoutManager == null ? 1 : layoutManager.getSpanCount()) {
2082 @Override
2083 public boolean canScrollVertically() { return getItemCount() > 1; }
2084 };
2085 layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
2086 @Override
2087 public int getSpanSize(int position) {
2088 if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
2089 return 1;
2090 }
2091 });
2092 return layoutManager;
2093 }
2094
2095 public void setBinding(CommandPageBinding b) {
2096 mBinding = b;
2097 mBinding.form.setLayoutManager(setupLayoutManager());
2098 mBinding.form.setAdapter(this);
2099 mBinding.actions.setAdapter(actionsAdapter);
2100 mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
2101 if (execute(pos)) {
2102 removeSession(CommandSession.this);
2103 }
2104 });
2105
2106 actionsAdapter.notifyDataSetChanged();
2107 }
2108 }
2109 }
2110}