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