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