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