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