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