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