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