1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.content.Context;
5import android.content.DialogInterface;
6import android.content.Intent;
7import android.database.Cursor;
8import android.database.DataSetObserver;
9import android.graphics.drawable.BitmapDrawable;
10import android.graphics.drawable.Drawable;
11import android.graphics.Bitmap;
12import android.graphics.Canvas;
13import android.graphics.Rect;
14import android.net.Uri;
15import android.telephony.PhoneNumberUtils;
16import android.text.Editable;
17import android.text.InputType;
18import android.text.SpannableStringBuilder;
19import android.text.Spanned;
20import android.text.StaticLayout;
21import android.text.TextPaint;
22import android.text.TextUtils;
23import android.text.TextWatcher;
24import android.view.LayoutInflater;
25import android.view.MotionEvent;
26import android.view.Gravity;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.AbsListView;
30import android.widget.ArrayAdapter;
31import android.widget.AdapterView;
32import android.widget.Button;
33import android.widget.CompoundButton;
34import android.widget.GridLayout;
35import android.widget.ListView;
36import android.widget.TextView;
37import android.widget.Toast;
38import android.widget.Spinner;
39import android.webkit.JavascriptInterface;
40import android.webkit.WebMessage;
41import android.webkit.WebView;
42import android.webkit.WebViewClient;
43import android.webkit.WebChromeClient;
44import android.util.DisplayMetrics;
45import android.util.LruCache;
46import android.util.Pair;
47import android.util.SparseArray;
48
49import androidx.annotation.NonNull;
50import androidx.annotation.Nullable;
51import androidx.appcompat.app.AlertDialog;
52import androidx.appcompat.app.AlertDialog.Builder;
53import androidx.core.content.ContextCompat;
54import androidx.databinding.DataBindingUtil;
55import androidx.databinding.ViewDataBinding;
56import androidx.viewpager.widget.PagerAdapter;
57import androidx.recyclerview.widget.RecyclerView;
58import androidx.recyclerview.widget.GridLayoutManager;
59import androidx.viewpager.widget.ViewPager;
60
61import com.caverock.androidsvg.SVG;
62
63import com.cheogram.android.ConversationPage;
64import com.cheogram.android.Util;
65import com.cheogram.android.WebxdcPage;
66
67import com.google.android.material.tabs.TabLayout;
68import com.google.android.material.textfield.TextInputLayout;
69import com.google.common.base.Optional;
70import com.google.common.collect.ComparisonChain;
71import com.google.common.collect.Lists;
72
73import io.ipfs.cid.Cid;
74
75import io.michaelrocks.libphonenumber.android.NumberParseException;
76
77import org.json.JSONArray;
78import org.json.JSONException;
79import org.json.JSONObject;
80
81import java.time.LocalDateTime;
82import java.time.ZoneId;
83import java.time.ZonedDateTime;
84import java.time.format.DateTimeParseException;
85import java.time.format.DateTimeFormatter;
86import java.time.format.FormatStyle;
87import java.util.ArrayList;
88import java.util.Collections;
89import java.util.Iterator;
90import java.util.HashSet;
91import java.util.List;
92import java.util.ListIterator;
93import java.util.concurrent.atomic.AtomicBoolean;
94import java.util.stream.Collectors;
95import java.util.Set;
96import java.util.Timer;
97import java.util.TimerTask;
98
99import me.saket.bettermovementmethod.BetterLinkMovementMethod;
100
101import eu.siacs.conversations.Config;
102import eu.siacs.conversations.R;
103import eu.siacs.conversations.crypto.OmemoSetting;
104import eu.siacs.conversations.crypto.PgpDecryptionService;
105import eu.siacs.conversations.databinding.CommandButtonGridFieldBinding;
106import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
107import eu.siacs.conversations.databinding.CommandItemCardBinding;
108import eu.siacs.conversations.databinding.CommandNoteBinding;
109import eu.siacs.conversations.databinding.CommandPageBinding;
110import eu.siacs.conversations.databinding.CommandProgressBarBinding;
111import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
112import eu.siacs.conversations.databinding.CommandResultCellBinding;
113import eu.siacs.conversations.databinding.CommandResultFieldBinding;
114import eu.siacs.conversations.databinding.CommandSearchListFieldBinding;
115import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
116import eu.siacs.conversations.databinding.CommandTextFieldBinding;
117import eu.siacs.conversations.databinding.CommandWebviewBinding;
118import eu.siacs.conversations.databinding.DialogQuickeditBinding;
119import eu.siacs.conversations.http.HttpConnectionManager;
120import eu.siacs.conversations.persistance.DatabaseBackend;
121import eu.siacs.conversations.services.AvatarService;
122import eu.siacs.conversations.services.QuickConversationsService;
123import eu.siacs.conversations.services.XmppConnectionService;
124import eu.siacs.conversations.ui.UriHandlerActivity;
125import eu.siacs.conversations.ui.text.FixedURLSpan;
126import eu.siacs.conversations.ui.util.ShareUtil;
127import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
128import eu.siacs.conversations.utils.Consumer;
129import eu.siacs.conversations.utils.JidHelper;
130import eu.siacs.conversations.utils.MessageUtils;
131import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
132import eu.siacs.conversations.utils.UIHelper;
133import eu.siacs.conversations.xml.Element;
134import eu.siacs.conversations.xml.Namespace;
135import eu.siacs.conversations.xmpp.Jid;
136import eu.siacs.conversations.xmpp.chatstate.ChatState;
137import eu.siacs.conversations.xmpp.forms.Data;
138import eu.siacs.conversations.xmpp.forms.Option;
139import eu.siacs.conversations.xmpp.mam.MamReference;
140import eu.siacs.conversations.xmpp.stanzas.IqPacket;
141
142import static eu.siacs.conversations.entities.Bookmark.printableValue;
143
144
145public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
146 public static final String TABLENAME = "conversations";
147
148 public static final int STATUS_AVAILABLE = 0;
149 public static final int STATUS_ARCHIVED = 1;
150
151 public static final String NAME = "name";
152 public static final String ACCOUNT = "accountUuid";
153 public static final String CONTACT = "contactUuid";
154 public static final String CONTACTJID = "contactJid";
155 public static final String STATUS = "status";
156 public static final String CREATED = "created";
157 public static final String MODE = "mode";
158 public static final String ATTRIBUTES = "attributes";
159
160 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
161 public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
162 public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
163 public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
164 public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
165 static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
166 static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
167 static final String ATTRIBUTE_MODERATED = "moderated";
168 static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
169 private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
170 private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
171 private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
172 private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
173 private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
174 protected final ArrayList<Message> messages = new ArrayList<>();
175 public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
176 protected Account account = null;
177 private String draftMessage;
178 private final String name;
179 private final String contactUuid;
180 private final String accountUuid;
181 private Jid contactJid;
182 private int status;
183 private final long created;
184 private int mode;
185 private JSONObject attributes;
186 private Jid nextCounterpart;
187 private transient MucOptions mucOptions = null;
188 private boolean messagesLeftOnServer = true;
189 private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
190 private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
191 private String mFirstMamReference = null;
192 protected int mCurrentTab = -1;
193 protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
194 protected Element thread = null;
195 protected boolean lockThread = false;
196 protected boolean userSelectedThread = false;
197 protected Message replyTo = null;
198
199 public Conversation(final String name, final Account account, final Jid contactJid,
200 final int mode) {
201 this(java.util.UUID.randomUUID().toString(), name, null, account
202 .getUuid(), contactJid, System.currentTimeMillis(),
203 STATUS_AVAILABLE, mode, "");
204 this.account = account;
205 }
206
207 public Conversation(final String uuid, final String name, final String contactUuid,
208 final String accountUuid, final Jid contactJid, final long created, final int status,
209 final int mode, final String attributes) {
210 this.uuid = uuid;
211 this.name = name;
212 this.contactUuid = contactUuid;
213 this.accountUuid = accountUuid;
214 this.contactJid = contactJid;
215 this.created = created;
216 this.status = status;
217 this.mode = mode;
218 try {
219 this.attributes = new JSONObject(attributes == null ? "" : attributes);
220 } catch (JSONException e) {
221 this.attributes = new JSONObject();
222 }
223 }
224
225 public static Conversation fromCursor(Cursor cursor) {
226 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
227 cursor.getString(cursor.getColumnIndex(NAME)),
228 cursor.getString(cursor.getColumnIndex(CONTACT)),
229 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
230 JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
231 cursor.getLong(cursor.getColumnIndex(CREATED)),
232 cursor.getInt(cursor.getColumnIndex(STATUS)),
233 cursor.getInt(cursor.getColumnIndex(MODE)),
234 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
235 }
236
237 public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
238 for (int i = messages.size() - 1; i >= 0; --i) {
239 final Message message = messages.get(i);
240 if (message.getStatus() <= Message.STATUS_RECEIVED
241 && (message.markable || isPrivateAndNonAnonymousMuc)
242 && !message.isPrivateMessage()) {
243 return message;
244 }
245 }
246 return null;
247 }
248
249 private static boolean suitableForOmemoByDefault(final Conversation conversation) {
250 if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
251 return false;
252 }
253 if (conversation.getContact().isOwnServer()) {
254 return false;
255 }
256 final String contact = conversation.getJid().getDomain().toEscapedString();
257 final String account = conversation.getAccount().getServer();
258 if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
259 return false;
260 }
261 return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
262 }
263
264 public boolean hasMessagesLeftOnServer() {
265 return messagesLeftOnServer;
266 }
267
268 public void setHasMessagesLeftOnServer(boolean value) {
269 this.messagesLeftOnServer = value;
270 }
271
272 public Message getFirstUnreadMessage() {
273 Message first = null;
274 synchronized (this.messages) {
275 for (int i = messages.size() - 1; i >= 0; --i) {
276 if (messages.get(i).isRead()) {
277 return first;
278 } else {
279 first = messages.get(i);
280 }
281 }
282 }
283 return first;
284 }
285
286 public String findMostRecentRemoteDisplayableId() {
287 final boolean multi = mode == Conversation.MODE_MULTI;
288 synchronized (this.messages) {
289 for (final Message message : Lists.reverse(this.messages)) {
290 if (message.getStatus() == Message.STATUS_RECEIVED) {
291 final String serverMsgId = message.getServerMsgId();
292 if (serverMsgId != null && multi) {
293 return serverMsgId;
294 }
295 return message.getRemoteMsgId();
296 }
297 }
298 }
299 return null;
300 }
301
302 public int countFailedDeliveries() {
303 int count = 0;
304 synchronized (this.messages) {
305 for(final Message message : this.messages) {
306 if (message.getStatus() == Message.STATUS_SEND_FAILED) {
307 ++count;
308 }
309 }
310 }
311 return count;
312 }
313
314 public Message getLastEditableMessage() {
315 synchronized (this.messages) {
316 for (final Message message : Lists.reverse(this.messages)) {
317 if (message.isEditable()) {
318 if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
319 return null;
320 }
321 return message;
322 }
323 }
324 }
325 return null;
326 }
327
328
329 public Message findUnsentMessageWithUuid(String uuid) {
330 synchronized (this.messages) {
331 for (final Message message : this.messages) {
332 final int s = message.getStatus();
333 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
334 return message;
335 }
336 }
337 }
338 return null;
339 }
340
341 public void findWaitingMessages(OnMessageFound onMessageFound) {
342 final ArrayList<Message> results = new ArrayList<>();
343 synchronized (this.messages) {
344 for (Message message : this.messages) {
345 if (message.getStatus() == Message.STATUS_WAITING) {
346 results.add(message);
347 }
348 }
349 }
350 for (Message result : results) {
351 onMessageFound.onMessageFound(result);
352 }
353 }
354
355 public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) {
356 final ArrayList<Message> results = new ArrayList<>();
357 synchronized (this.messages) {
358 for (final Message message : this.messages) {
359 if (message.isRead()) {
360 continue;
361 }
362 results.add(message);
363 }
364 }
365 for (final Message result : results) {
366 onMessageFound.onMessageFound(result);
367 }
368 }
369
370 public Message findMessageWithFileAndUuid(final String uuid) {
371 synchronized (this.messages) {
372 for (final Message message : this.messages) {
373 final Transferable transferable = message.getTransferable();
374 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
375 if (message.getUuid().equals(uuid)
376 && message.getEncryption() != Message.ENCRYPTION_PGP
377 && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
378 return message;
379 }
380 }
381 }
382 return null;
383 }
384
385 public Message findMessageWithUuid(final String uuid) {
386 synchronized (this.messages) {
387 for (final Message message : this.messages) {
388 if (message.getUuid().equals(uuid)) {
389 return message;
390 }
391 }
392 }
393 return null;
394 }
395
396 public boolean markAsDeleted(final List<String> uuids) {
397 boolean deleted = false;
398 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
399 synchronized (this.messages) {
400 for (Message message : this.messages) {
401 if (uuids.contains(message.getUuid())) {
402 message.setDeleted(true);
403 deleted = true;
404 if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
405 pgpDecryptionService.discard(message);
406 }
407 }
408 }
409 }
410 return deleted;
411 }
412
413 public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
414 boolean changed = false;
415 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
416 synchronized (this.messages) {
417 for (Message message : this.messages) {
418 for (final DatabaseBackend.FilePathInfo file : files)
419 if (file.uuid.toString().equals(message.getUuid())) {
420 message.setDeleted(file.deleted);
421 changed = true;
422 if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
423 pgpDecryptionService.discard(message);
424 }
425 }
426 }
427 }
428 return changed;
429 }
430
431 public void clearMessages() {
432 synchronized (this.messages) {
433 this.messages.clear();
434 }
435 }
436
437 public boolean setIncomingChatState(ChatState state) {
438 if (this.mIncomingChatState == state) {
439 return false;
440 }
441 this.mIncomingChatState = state;
442 return true;
443 }
444
445 public ChatState getIncomingChatState() {
446 return this.mIncomingChatState;
447 }
448
449 public boolean setOutgoingChatState(ChatState state) {
450 if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
451 if (this.mOutgoingChatState != state) {
452 this.mOutgoingChatState = state;
453 return true;
454 }
455 }
456 return false;
457 }
458
459 public ChatState getOutgoingChatState() {
460 return this.mOutgoingChatState;
461 }
462
463 public void trim() {
464 synchronized (this.messages) {
465 final int size = messages.size();
466 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
467 if (size > maxsize) {
468 List<Message> discards = this.messages.subList(0, size - maxsize);
469 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
470 if (pgpDecryptionService != null) {
471 pgpDecryptionService.discard(discards);
472 }
473 discards.clear();
474 untieMessages();
475 }
476 }
477 }
478
479 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
480 final ArrayList<Message> results = new ArrayList<>();
481 synchronized (this.messages) {
482 for (Message message : this.messages) {
483 if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
484 results.add(message);
485 }
486 }
487 }
488 for (Message result : results) {
489 onMessageFound.onMessageFound(result);
490 }
491 }
492
493 public Message findSentMessageWithUuidOrRemoteId(String id) {
494 synchronized (this.messages) {
495 for (Message message : this.messages) {
496 if (id.equals(message.getUuid())
497 || (message.getStatus() >= Message.STATUS_SEND
498 && id.equals(message.getRemoteMsgId()))) {
499 return message;
500 }
501 }
502 }
503 return null;
504 }
505
506 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
507 synchronized (this.messages) {
508 for (int i = this.messages.size() - 1; i >= 0; --i) {
509 final Message message = messages.get(i);
510 final Jid mcp = message.getCounterpart();
511 if (mcp == null && counterpart != null) {
512 continue;
513 }
514 if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
515 final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
516 if (idMatch) return message;
517 }
518 }
519 }
520 return null;
521 }
522
523 public Message findSentMessageWithUuid(String id) {
524 synchronized (this.messages) {
525 for (Message message : this.messages) {
526 if (id.equals(message.getUuid())) {
527 return message;
528 }
529 }
530 }
531 return null;
532 }
533
534 public Message findMessageWithRemoteId(String id, Jid counterpart) {
535 synchronized (this.messages) {
536 for (Message message : this.messages) {
537 if (counterpart.equals(message.getCounterpart())
538 && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
539 return message;
540 }
541 }
542 }
543 return null;
544 }
545
546 public Message findMessageWithServerMsgId(String id) {
547 synchronized (this.messages) {
548 for (Message message : this.messages) {
549 if (id != null && id.equals(message.getServerMsgId())) {
550 return message;
551 }
552 }
553 }
554 return null;
555 }
556
557 public boolean hasMessageWithCounterpart(Jid counterpart) {
558 synchronized (this.messages) {
559 for (Message message : this.messages) {
560 if (counterpart.equals(message.getCounterpart())) {
561 return true;
562 }
563 }
564 }
565 return false;
566 }
567
568 public Message findMessageReactingTo(String id, Jid reactor) {
569 if (id == null) return null;
570
571 synchronized (this.messages) {
572 for (int i = this.messages.size() - 1; i >= 0; --i) {
573 final Message message = messages.get(i);
574 if (reactor == null && message.getStatus() < Message.STATUS_SEND) continue;
575 if (reactor != null && !(message.getCounterpart().equals(reactor) || message.getCounterpart().asBareJid().equals(reactor))) continue;
576
577 final Element r = message.getReactions();
578 if (r != null && r.getAttribute("id") != null && id.equals(r.getAttribute("id"))) {
579 return message;
580 }
581 }
582 }
583 return null;
584 }
585
586 public Set<String> findReactionsTo(String id, Jid reactor) {
587 Set<String> reactionEmoji = new HashSet<>();
588 Message reactM = findMessageReactingTo(id, reactor);
589 Element reactions = reactM == null ? null : reactM.getReactions();
590 if (reactions != null) {
591 for (Element el : reactions.getChildren()) {
592 if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
593 reactionEmoji.add(el.getContent());
594 }
595 }
596 }
597 return reactionEmoji;
598 }
599
600 public long loadMoreTimestamp() {
601 if (messages.size() < 1) return 0;
602 if (getLockThread() && messages.size() > 5000) return 0;
603
604 if (messages.get(0).getType() == Message.TYPE_STATUS && messages.size() >= 2) {
605 return messages.get(1).getTimeSent();
606 } else {
607 return messages.get(0).getTimeSent();
608 }
609 }
610
611 public void populateWithMessages(final List<Message> messages) {
612 synchronized (this.messages) {
613 messages.clear();
614 messages.addAll(this.messages);
615 }
616 Set<String> extraIds = new HashSet<>();
617 for (ListIterator<Message> iterator = messages.listIterator(messages.size()); iterator.hasPrevious(); ) {
618 Message m = iterator.previous();
619 if (m.wasMergedIntoPrevious() || (getLockThread() && !extraIds.contains(m.replyId()) && (m.getThread() == null || !m.getThread().getContent().equals(getThread().getContent())))) {
620 iterator.remove();
621 } else if (getLockThread() && m.getThread() != null) {
622 Element reply = m.getReply();
623 if (reply != null && reply.getAttribute("id") != null) extraIds.add(reply.getAttribute("id"));
624 Element reactions = m.getReactions();
625 if (reactions != null && reactions.getAttribute("id") != null) extraIds.add(reactions.getAttribute("id"));
626 }
627 }
628 }
629
630 @Override
631 public boolean isBlocked() {
632 return getContact().isBlocked();
633 }
634
635 @Override
636 public boolean isDomainBlocked() {
637 return getContact().isDomainBlocked();
638 }
639
640 @Override
641 public Jid getBlockedJid() {
642 return getContact().getBlockedJid();
643 }
644
645 public int countMessages() {
646 synchronized (this.messages) {
647 return this.messages.size();
648 }
649 }
650
651 public String getFirstMamReference() {
652 return this.mFirstMamReference;
653 }
654
655 public void setFirstMamReference(String reference) {
656 this.mFirstMamReference = reference;
657 }
658
659 public void setLastClearHistory(long time, String reference) {
660 if (reference != null) {
661 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
662 } else {
663 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
664 }
665 }
666
667 public MamReference getLastClearHistory() {
668 return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
669 }
670
671 public List<Jid> getAcceptedCryptoTargets() {
672 if (mode == MODE_SINGLE) {
673 return Collections.singletonList(getJid().asBareJid());
674 } else {
675 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
676 }
677 }
678
679 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
680 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
681 }
682
683 public boolean setCorrectingMessage(Message correctingMessage) {
684 setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
685 return correctingMessage == null && draftMessage != null;
686 }
687
688 public Message getCorrectingMessage() {
689 final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
690 return uuid == null ? null : findSentMessageWithUuid(uuid);
691 }
692
693 public boolean withSelf() {
694 return getContact().isSelf();
695 }
696
697 @Override
698 public int compareTo(@NonNull Conversation another) {
699 return ComparisonChain.start()
700 .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
701 .compare(another.getSortableTime(), getSortableTime())
702 .result();
703 }
704
705 public long getSortableTime() {
706 Draft draft = getDraft();
707 long messageTime = getLatestMessage().getTimeReceived();
708 if (draft == null) {
709 return messageTime;
710 } else {
711 return Math.max(messageTime, draft.getTimestamp());
712 }
713 }
714
715 public String getDraftMessage() {
716 return draftMessage;
717 }
718
719 public void setDraftMessage(String draftMessage) {
720 this.draftMessage = draftMessage;
721 }
722
723 public Element getThread() {
724 return this.thread;
725 }
726
727 public void setThread(Element thread) {
728 this.thread = thread;
729 }
730
731 public void setLockThread(boolean flag) {
732 this.lockThread = flag;
733 if (flag) setUserSelectedThread(true);
734 }
735
736 public boolean getLockThread() {
737 return this.lockThread;
738 }
739
740 public void setUserSelectedThread(boolean flag) {
741 this.userSelectedThread = flag;
742 }
743
744 public boolean getUserSelectedThread() {
745 return this.userSelectedThread;
746 }
747
748 public void setReplyTo(Message m) {
749 this.replyTo = m;
750 }
751
752 public Message getReplyTo() {
753 return this.replyTo;
754 }
755
756 public boolean isRead() {
757 synchronized (this.messages) {
758 for(final Message message : Lists.reverse(this.messages)) {
759 if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
760 continue;
761 }
762 return message.isRead();
763 }
764 return true;
765 }
766 }
767
768 public List<Message> markRead(String upToUuid) {
769 final List<Message> unread = new ArrayList<>();
770 synchronized (this.messages) {
771 for (Message message : this.messages) {
772 if (!message.isRead()) {
773 message.markRead();
774 unread.add(message);
775 }
776 if (message.getUuid().equals(upToUuid)) {
777 return unread;
778 }
779 }
780 }
781 return unread;
782 }
783
784 public Message getLatestMessage() {
785 synchronized (this.messages) {
786 if (this.messages.size() == 0) {
787 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
788 message.setType(Message.TYPE_STATUS);
789 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
790 message.setTimeReceived(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
791 return message;
792 } else {
793 return this.messages.get(this.messages.size() - 1);
794 }
795 }
796 }
797
798 public @NonNull
799 CharSequence getName() {
800 if (getMode() == MODE_MULTI) {
801 final String roomName = getMucOptions().getName();
802 final String subject = getMucOptions().getSubject();
803 final Bookmark bookmark = getBookmark();
804 final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
805 if (printableValue(roomName)) {
806 return roomName;
807 } else if (printableValue(subject)) {
808 return subject;
809 } else if (printableValue(bookmarkName, false)) {
810 return bookmarkName;
811 } else {
812 final String generatedName = getMucOptions().createNameFromParticipants();
813 if (printableValue(generatedName)) {
814 return generatedName;
815 } else {
816 return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
817 }
818 }
819 } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
820 return contactJid;
821 } else {
822 return this.getContact().getDisplayName();
823 }
824 }
825
826 public String getAccountUuid() {
827 return this.accountUuid;
828 }
829
830 public Account getAccount() {
831 return this.account;
832 }
833
834 public void setAccount(final Account account) {
835 this.account = account;
836 }
837
838 public Contact getContact() {
839 return this.account.getRoster().getContact(this.contactJid);
840 }
841
842 @Override
843 public Jid getJid() {
844 return this.contactJid;
845 }
846
847 public int getStatus() {
848 return this.status;
849 }
850
851 public void setStatus(int status) {
852 this.status = status;
853 }
854
855 public long getCreated() {
856 return this.created;
857 }
858
859 public ContentValues getContentValues() {
860 ContentValues values = new ContentValues();
861 values.put(UUID, uuid);
862 values.put(NAME, name);
863 values.put(CONTACT, contactUuid);
864 values.put(ACCOUNT, accountUuid);
865 values.put(CONTACTJID, contactJid.toString());
866 values.put(CREATED, created);
867 values.put(STATUS, status);
868 values.put(MODE, mode);
869 synchronized (this.attributes) {
870 values.put(ATTRIBUTES, attributes.toString());
871 }
872 return values;
873 }
874
875 public int getMode() {
876 return this.mode;
877 }
878
879 public void setMode(int mode) {
880 this.mode = mode;
881 }
882
883 /**
884 * short for is Private and Non-anonymous
885 */
886 public boolean isSingleOrPrivateAndNonAnonymous() {
887 return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
888 }
889
890 public boolean isPrivateAndNonAnonymous() {
891 return getMucOptions().isPrivateAndNonAnonymous();
892 }
893
894 public synchronized MucOptions getMucOptions() {
895 if (this.mucOptions == null) {
896 this.mucOptions = new MucOptions(this);
897 }
898 return this.mucOptions;
899 }
900
901 public void resetMucOptions() {
902 this.mucOptions = null;
903 }
904
905 public void setContactJid(final Jid jid) {
906 this.contactJid = jid;
907 }
908
909 public Jid getNextCounterpart() {
910 return this.nextCounterpart;
911 }
912
913 public void setNextCounterpart(Jid jid) {
914 this.nextCounterpart = jid;
915 }
916
917 public int getNextEncryption() {
918 if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
919 return Message.ENCRYPTION_NONE;
920 }
921 if (OmemoSetting.isAlways()) {
922 return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
923 }
924 final int defaultEncryption;
925 if (suitableForOmemoByDefault(this)) {
926 defaultEncryption = OmemoSetting.getEncryption();
927 } else {
928 defaultEncryption = Message.ENCRYPTION_NONE;
929 }
930 int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
931 if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
932 return defaultEncryption;
933 } else {
934 return encryption;
935 }
936 }
937
938 public boolean setNextEncryption(int encryption) {
939 return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
940 }
941
942 public String getNextMessage() {
943 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
944 return nextMessage == null ? "" : nextMessage;
945 }
946
947 public @Nullable
948 Draft getDraft() {
949 long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
950 if (timestamp > getLatestMessage().getTimeSent()) {
951 String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
952 if (!TextUtils.isEmpty(message) && timestamp != 0) {
953 return new Draft(message, timestamp);
954 }
955 }
956 return null;
957 }
958
959 public boolean setNextMessage(final String input) {
960 final String message = input == null || input.trim().isEmpty() ? null : input;
961 boolean changed = !getNextMessage().equals(message);
962 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
963 if (changed) {
964 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
965 }
966 return changed;
967 }
968
969 public Bookmark getBookmark() {
970 return this.account.getBookmark(this.contactJid);
971 }
972
973 public Message findDuplicateMessage(Message message) {
974 synchronized (this.messages) {
975 for (int i = this.messages.size() - 1; i >= 0; --i) {
976 if (this.messages.get(i).similar(message)) {
977 return this.messages.get(i);
978 }
979 }
980 }
981 return null;
982 }
983
984 public boolean hasDuplicateMessage(Message message) {
985 return findDuplicateMessage(message) != null;
986 }
987
988 public Message findSentMessageWithBody(String body) {
989 synchronized (this.messages) {
990 for (int i = this.messages.size() - 1; i >= 0; --i) {
991 Message message = this.messages.get(i);
992 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
993 String otherBody;
994 if (message.hasFileOnRemoteHost()) {
995 otherBody = message.getFileParams().url;
996 } else {
997 otherBody = message.body;
998 }
999 if (otherBody != null && otherBody.equals(body)) {
1000 return message;
1001 }
1002 }
1003 }
1004 return null;
1005 }
1006 }
1007
1008 public Message findRtpSession(final String sessionId, final int s) {
1009 synchronized (this.messages) {
1010 for (int i = this.messages.size() - 1; i >= 0; --i) {
1011 final Message message = this.messages.get(i);
1012 if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
1013 return message;
1014 }
1015 }
1016 }
1017 return null;
1018 }
1019
1020 public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
1021 if (serverMsgId == null || remoteMsgId == null) {
1022 return false;
1023 }
1024 synchronized (this.messages) {
1025 for (Message message : this.messages) {
1026 if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
1027 return true;
1028 }
1029 }
1030 }
1031 return false;
1032 }
1033
1034 public MamReference getLastMessageTransmitted() {
1035 final MamReference lastClear = getLastClearHistory();
1036 MamReference lastReceived = new MamReference(0);
1037 synchronized (this.messages) {
1038 for (int i = this.messages.size() - 1; i >= 0; --i) {
1039 final Message message = this.messages.get(i);
1040 if (message.isPrivateMessage()) {
1041 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
1042 }
1043 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
1044 lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
1045 break;
1046 }
1047 }
1048 }
1049 return MamReference.max(lastClear, lastReceived);
1050 }
1051
1052 public void setMutedTill(long value) {
1053 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
1054 }
1055
1056 public boolean isMuted() {
1057 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
1058 }
1059
1060 public boolean alwaysNotify() {
1061 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
1062 }
1063
1064 public boolean setAttribute(String key, boolean value) {
1065 return setAttribute(key, String.valueOf(value));
1066 }
1067
1068 private boolean setAttribute(String key, long value) {
1069 return setAttribute(key, Long.toString(value));
1070 }
1071
1072 private boolean setAttribute(String key, int value) {
1073 return setAttribute(key, String.valueOf(value));
1074 }
1075
1076 public boolean setAttribute(String key, String value) {
1077 synchronized (this.attributes) {
1078 try {
1079 if (value == null) {
1080 if (this.attributes.has(key)) {
1081 this.attributes.remove(key);
1082 return true;
1083 } else {
1084 return false;
1085 }
1086 } else {
1087 final String prev = this.attributes.optString(key, null);
1088 this.attributes.put(key, value);
1089 return !value.equals(prev);
1090 }
1091 } catch (JSONException e) {
1092 throw new AssertionError(e);
1093 }
1094 }
1095 }
1096
1097 public boolean setAttribute(String key, List<Jid> jids) {
1098 JSONArray array = new JSONArray();
1099 for (Jid jid : jids) {
1100 array.put(jid.asBareJid().toString());
1101 }
1102 synchronized (this.attributes) {
1103 try {
1104 this.attributes.put(key, array);
1105 return true;
1106 } catch (JSONException e) {
1107 return false;
1108 }
1109 }
1110 }
1111
1112 public String getAttribute(String key) {
1113 synchronized (this.attributes) {
1114 return this.attributes.optString(key, null);
1115 }
1116 }
1117
1118 private List<Jid> getJidListAttribute(String key) {
1119 ArrayList<Jid> list = new ArrayList<>();
1120 synchronized (this.attributes) {
1121 try {
1122 JSONArray array = this.attributes.getJSONArray(key);
1123 for (int i = 0; i < array.length(); ++i) {
1124 try {
1125 list.add(Jid.of(array.getString(i)));
1126 } catch (IllegalArgumentException e) {
1127 //ignored
1128 }
1129 }
1130 } catch (JSONException e) {
1131 //ignored
1132 }
1133 }
1134 return list;
1135 }
1136
1137 private int getIntAttribute(String key, int defaultValue) {
1138 String value = this.getAttribute(key);
1139 if (value == null) {
1140 return defaultValue;
1141 } else {
1142 try {
1143 return Integer.parseInt(value);
1144 } catch (NumberFormatException e) {
1145 return defaultValue;
1146 }
1147 }
1148 }
1149
1150 public long getLongAttribute(String key, long defaultValue) {
1151 String value = this.getAttribute(key);
1152 if (value == null) {
1153 return defaultValue;
1154 } else {
1155 try {
1156 return Long.parseLong(value);
1157 } catch (NumberFormatException e) {
1158 return defaultValue;
1159 }
1160 }
1161 }
1162
1163 public boolean getBooleanAttribute(String key, boolean defaultValue) {
1164 String value = this.getAttribute(key);
1165 if (value == null) {
1166 return defaultValue;
1167 } else {
1168 return Boolean.parseBoolean(value);
1169 }
1170 }
1171
1172 public void add(Message message) {
1173 synchronized (this.messages) {
1174 this.messages.add(message);
1175 }
1176 }
1177
1178 public void prepend(int offset, Message message) {
1179 synchronized (this.messages) {
1180 this.messages.add(Math.min(offset, this.messages.size()), message);
1181 }
1182 }
1183
1184 public void addAll(int index, List<Message> messages) {
1185 synchronized (this.messages) {
1186 this.messages.addAll(index, messages);
1187 }
1188 account.getPgpDecryptionService().decrypt(messages);
1189 }
1190
1191 public void expireOldMessages(long timestamp) {
1192 synchronized (this.messages) {
1193 for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
1194 if (iterator.next().getTimeSent() < timestamp) {
1195 iterator.remove();
1196 }
1197 }
1198 untieMessages();
1199 }
1200 }
1201
1202 public void sort() {
1203 synchronized (this.messages) {
1204 Collections.sort(this.messages, (left, right) -> {
1205 if (left.getTimeSent() < right.getTimeSent()) {
1206 return -1;
1207 } else if (left.getTimeSent() > right.getTimeSent()) {
1208 return 1;
1209 } else {
1210 return 0;
1211 }
1212 });
1213 untieMessages();
1214 }
1215 }
1216
1217 private void untieMessages() {
1218 for (Message message : this.messages) {
1219 message.untie();
1220 }
1221 }
1222
1223 public int unreadCount() {
1224 synchronized (this.messages) {
1225 int count = 0;
1226 for(final Message message : Lists.reverse(this.messages)) {
1227 if (message.isRead()) {
1228 if (message.getType() == Message.TYPE_RTP_SESSION) {
1229 continue;
1230 }
1231 return count;
1232 }
1233 ++count;
1234 }
1235 return count;
1236 }
1237 }
1238
1239 public int receivedMessagesCount() {
1240 int count = 0;
1241 synchronized (this.messages) {
1242 for (Message message : messages) {
1243 if (message.getStatus() == Message.STATUS_RECEIVED) {
1244 ++count;
1245 }
1246 }
1247 }
1248 return count;
1249 }
1250
1251 public int sentMessagesCount() {
1252 int count = 0;
1253 synchronized (this.messages) {
1254 for (Message message : messages) {
1255 if (message.getStatus() != Message.STATUS_RECEIVED) {
1256 ++count;
1257 }
1258 }
1259 }
1260 return count;
1261 }
1262
1263 public boolean canInferPresence() {
1264 final Contact contact = getContact();
1265 if (contact != null && contact.canInferPresence()) return true;
1266 return sentMessagesCount() > 0;
1267 }
1268
1269 public boolean isWithStranger() {
1270 final Contact contact = getContact();
1271 return mode == MODE_SINGLE
1272 && !contact.isOwnServer()
1273 && !contact.showInContactList()
1274 && !contact.isSelf()
1275 && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
1276 && sentMessagesCount() == 0;
1277 }
1278
1279 public int getReceivedMessagesCountSinceUuid(String uuid) {
1280 if (uuid == null) {
1281 return 0;
1282 }
1283 int count = 0;
1284 synchronized (this.messages) {
1285 for (int i = messages.size() - 1; i >= 0; i--) {
1286 final Message message = messages.get(i);
1287 if (uuid.equals(message.getUuid())) {
1288 return count;
1289 }
1290 if (message.getStatus() <= Message.STATUS_RECEIVED) {
1291 ++count;
1292 }
1293 }
1294 }
1295 return 0;
1296 }
1297
1298 @Override
1299 public int getAvatarBackgroundColor() {
1300 return UIHelper.getColorForName(getName().toString());
1301 }
1302
1303 @Override
1304 public String getAvatarName() {
1305 return getName().toString();
1306 }
1307
1308 public void setCurrentTab(int tab) {
1309 mCurrentTab = tab;
1310 }
1311
1312 public int getCurrentTab() {
1313 if (mCurrentTab >= 0) return mCurrentTab;
1314
1315 if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
1316 return 0;
1317 }
1318
1319 return 1;
1320 }
1321
1322 public void refreshSessions() {
1323 pagerAdapter.refreshSessions();
1324 }
1325
1326 public void startWebxdc(WebxdcPage page) {
1327 pagerAdapter.startWebxdc(page);
1328 }
1329
1330 public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1331 pagerAdapter.startCommand(command, xmppConnectionService);
1332 }
1333
1334 public boolean switchToSession(final String node) {
1335 return pagerAdapter.switchToSession(node);
1336 }
1337
1338 public void setupViewPager(ViewPager pager, TabLayout tabs, boolean onboarding, Conversation oldConversation) {
1339 pagerAdapter.setupViewPager(pager, tabs, onboarding, oldConversation);
1340 }
1341
1342 public void showViewPager() {
1343 pagerAdapter.show();
1344 }
1345
1346 public void hideViewPager() {
1347 pagerAdapter.hide();
1348 }
1349
1350 public interface OnMessageFound {
1351 void onMessageFound(final Message message);
1352 }
1353
1354 public static class Draft {
1355 private final String message;
1356 private final long timestamp;
1357
1358 private Draft(String message, long timestamp) {
1359 this.message = message;
1360 this.timestamp = timestamp;
1361 }
1362
1363 public long getTimestamp() {
1364 return timestamp;
1365 }
1366
1367 public String getMessage() {
1368 return message;
1369 }
1370 }
1371
1372 public class ConversationPagerAdapter extends PagerAdapter {
1373 protected ViewPager mPager = null;
1374 protected TabLayout mTabs = null;
1375 ArrayList<ConversationPage> sessions = null;
1376 protected View page1 = null;
1377 protected View page2 = null;
1378 protected boolean mOnboarding = false;
1379
1380 public void setupViewPager(ViewPager pager, TabLayout tabs, boolean onboarding, Conversation oldConversation) {
1381 mPager = pager;
1382 mTabs = tabs;
1383 mOnboarding = onboarding;
1384
1385 if (oldConversation != null) {
1386 oldConversation.pagerAdapter.mPager = null;
1387 oldConversation.pagerAdapter.mTabs = null;
1388 }
1389
1390 if (mPager == null) return;
1391 if (sessions != null) show();
1392
1393 if (pager.getChildAt(0) != null) page1 = pager.getChildAt(0);
1394 if (pager.getChildAt(1) != null) page2 = pager.getChildAt(1);
1395 if (page2 != null && page2.findViewById(R.id.commands_view) == null) {
1396 page1 = null;
1397 page2 = null;
1398 }
1399 if (page1 == null) page1 = oldConversation.pagerAdapter.page1;
1400 if (page2 == null) page2 = oldConversation.pagerAdapter.page2;
1401 if (page1 == null || page2 == null) {
1402 throw new IllegalStateException("page1 or page2 were not present as child or in model?");
1403 }
1404 pager.removeView(page1);
1405 pager.removeView(page2);
1406 pager.setAdapter(this);
1407 tabs.setupWithViewPager(mPager);
1408 pager.post(() -> pager.setCurrentItem(getCurrentTab()));
1409
1410 mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
1411 public void onPageScrollStateChanged(int state) { }
1412 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
1413
1414 public void onPageSelected(int position) {
1415 setCurrentTab(position);
1416 }
1417 });
1418 }
1419
1420 public void show() {
1421 if (sessions == null) {
1422 sessions = new ArrayList<>();
1423 notifyDataSetChanged();
1424 }
1425 if (!mOnboarding && mTabs != null) mTabs.setVisibility(View.VISIBLE);
1426 }
1427
1428 public void hide() {
1429 if (sessions != null && !sessions.isEmpty()) return; // Do not hide during active session
1430 if (mPager != null) mPager.setCurrentItem(0);
1431 if (mTabs != null) mTabs.setVisibility(View.GONE);
1432 sessions = null;
1433 notifyDataSetChanged();
1434 }
1435
1436 public void refreshSessions() {
1437 if (sessions == null) return;
1438
1439 for (ConversationPage session : sessions) {
1440 session.refresh();
1441 }
1442 }
1443
1444 public void startWebxdc(WebxdcPage page) {
1445 show();
1446 sessions.add(page);
1447 notifyDataSetChanged();
1448 if (mPager != null) mPager.setCurrentItem(getCount() - 1);
1449 }
1450
1451 public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
1452 show();
1453 CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
1454
1455 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
1456 packet.setTo(command.getAttributeAsJid("jid"));
1457 final Element c = packet.addChild("command", Namespace.COMMANDS);
1458 c.setAttribute("node", command.getAttribute("node"));
1459 c.setAttribute("action", "execute");
1460
1461 final TimerTask task = new TimerTask() {
1462 @Override
1463 public void run() {
1464 if (getAccount().getStatus() != Account.State.ONLINE) {
1465 final TimerTask self = this;
1466 new Timer().schedule(new TimerTask() {
1467 @Override
1468 public void run() {
1469 self.run();
1470 }
1471 }, 1000);
1472 } else {
1473 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
1474 session.updateWithResponse(iq);
1475 });
1476 }
1477 }
1478 };
1479
1480 if (command.getAttribute("node").equals("jabber:iq:register") && packet.getTo().asBareJid().equals(Jid.of("cheogram.com"))) {
1481 new com.cheogram.android.CheogramLicenseChecker(mPager.getContext(), (signedData, signature) -> {
1482 if (signedData != null && signature != null) {
1483 c.addChild("license", "https://ns.cheogram.com/google-play").setContent(signedData);
1484 c.addChild("licenseSignature", "https://ns.cheogram.com/google-play").setContent(signature);
1485 }
1486
1487 task.run();
1488 }).checkLicense();
1489 } else {
1490 task.run();
1491 }
1492
1493 sessions.add(session);
1494 notifyDataSetChanged();
1495 if (mPager != null) mPager.setCurrentItem(getCount() - 1);
1496 }
1497
1498 public void removeSession(ConversationPage session) {
1499 sessions.remove(session);
1500 notifyDataSetChanged();
1501 if (session instanceof WebxdcPage) mPager.setCurrentItem(0);
1502 }
1503
1504 public boolean switchToSession(final String node) {
1505 if (sessions == null) return false;
1506
1507 int i = 0;
1508 for (ConversationPage session : sessions) {
1509 if (session.getNode().equals(node)) {
1510 if (mPager != null) mPager.setCurrentItem(i + 2);
1511 return true;
1512 }
1513 i++;
1514 }
1515
1516 return false;
1517 }
1518
1519 @NonNull
1520 @Override
1521 public Object instantiateItem(@NonNull ViewGroup container, int position) {
1522 if (position == 0) {
1523 if (page1 != null && page1.getParent() != null) {
1524 ((ViewGroup) page1.getParent()).removeView(page1);
1525 }
1526 container.addView(page1);
1527 return page1;
1528 }
1529 if (position == 1) {
1530 if (page2 != null && page2.getParent() != null) {
1531 ((ViewGroup) page2.getParent()).removeView(page2);
1532 }
1533 container.addView(page2);
1534 return page2;
1535 }
1536
1537 ConversationPage session = sessions.get(position-2);
1538 View v = session.inflateUi(container.getContext(), (s) -> removeSession(s));
1539 if (v != null && v.getParent() != null) {
1540 ((ViewGroup) v.getParent()).removeView(v);
1541 }
1542 container.addView(v);
1543 return session;
1544 }
1545
1546 @Override
1547 public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
1548 if (position < 2) {
1549 container.removeView((View) o);
1550 return;
1551 }
1552
1553 container.removeView(((ConversationPage) o).getView());
1554 }
1555
1556 @Override
1557 public int getItemPosition(Object o) {
1558 if (mPager != null) {
1559 if (o == page1) return PagerAdapter.POSITION_UNCHANGED;
1560 if (o == page2) return PagerAdapter.POSITION_UNCHANGED;
1561 }
1562
1563 int pos = sessions == null ? -1 : sessions.indexOf(o);
1564 if (pos < 0) return PagerAdapter.POSITION_NONE;
1565 return pos + 2;
1566 }
1567
1568 @Override
1569 public int getCount() {
1570 if (sessions == null) return 1;
1571
1572 int count = 2 + sessions.size();
1573 if (mTabs == null) return count;
1574
1575 if (count > 2) {
1576 mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
1577 } else {
1578 mTabs.setTabMode(TabLayout.MODE_FIXED);
1579 }
1580 return count;
1581 }
1582
1583 @Override
1584 public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
1585 if (view == o) return true;
1586
1587 if (o instanceof ConversationPage) {
1588 return ((ConversationPage) o).getView() == view;
1589 }
1590
1591 return false;
1592 }
1593
1594 @Nullable
1595 @Override
1596 public CharSequence getPageTitle(int position) {
1597 switch (position) {
1598 case 0:
1599 return "Conversation";
1600 case 1:
1601 return "Commands";
1602 default:
1603 ConversationPage session = sessions.get(position-2);
1604 if (session == null) return super.getPageTitle(position);
1605 return session.getTitle();
1606 }
1607 }
1608
1609 class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> implements ConversationPage {
1610 abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
1611 protected T binding;
1612
1613 public ViewHolder(T binding) {
1614 super(binding.getRoot());
1615 this.binding = binding;
1616 }
1617
1618 abstract public void bind(Item el);
1619
1620 protected void setTextOrHide(TextView v, Optional<String> s) {
1621 if (s == null || !s.isPresent()) {
1622 v.setVisibility(View.GONE);
1623 } else {
1624 v.setVisibility(View.VISIBLE);
1625 v.setText(s.get());
1626 }
1627 }
1628
1629 protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
1630 int flags = 0;
1631 if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
1632 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1633
1634 String type = field.getAttribute("type");
1635 if (type != null) {
1636 if (type.equals("text-multi") || type.equals("jid-multi")) {
1637 flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
1638 }
1639
1640 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
1641
1642 if (type.equals("jid-single") || type.equals("jid-multi")) {
1643 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1644 }
1645
1646 if (type.equals("text-private")) {
1647 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1648 if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
1649 }
1650 }
1651
1652 Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1653 if (validate == null) return;
1654 String datatype = validate.getAttribute("datatype");
1655 if (datatype == null) return;
1656
1657 if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
1658 textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
1659 }
1660
1661 if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
1662 textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
1663 }
1664
1665 if (datatype.equals("xs:date")) {
1666 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
1667 }
1668
1669 if (datatype.equals("xs:dateTime")) {
1670 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
1671 }
1672
1673 if (datatype.equals("xs:time")) {
1674 textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
1675 }
1676
1677 if (datatype.equals("xs:anyURI")) {
1678 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
1679 }
1680
1681 if (datatype.equals("html:tel")) {
1682 textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
1683 }
1684
1685 if (datatype.equals("html:email")) {
1686 textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
1687 }
1688 }
1689
1690 protected String formatValue(String datatype, String value, boolean compact) {
1691 if ("xs:dateTime".equals(datatype)) {
1692 ZonedDateTime zonedDateTime = null;
1693 try {
1694 zonedDateTime = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
1695 } catch (final DateTimeParseException e) {
1696 try {
1697 DateTimeFormatter almostIso = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm[:ss] X");
1698 zonedDateTime = ZonedDateTime.parse(value, almostIso);
1699 } catch (final DateTimeParseException e2) { }
1700 }
1701 if (zonedDateTime == null) return value;
1702 ZonedDateTime localZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.systemDefault());
1703 DateTimeFormatter outputFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
1704 return localZonedDateTime.toLocalDateTime().format(outputFormat);
1705 }
1706
1707 if ("html:tel".equals(datatype) && !compact) {
1708 return PhoneNumberUtils.formatNumber(value, value, null);
1709 }
1710
1711 return value;
1712 }
1713 }
1714
1715 class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
1716 public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
1717
1718 @Override
1719 public void bind(Item iq) {
1720 binding.errorIcon.setVisibility(View.VISIBLE);
1721
1722 Element error = iq.el.findChild("error");
1723 if (error == null) return;
1724 String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
1725 if (text == null || text.equals("")) {
1726 text = error.getChildren().get(0).getName();
1727 }
1728 binding.message.setText(text);
1729 }
1730 }
1731
1732 class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
1733 public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
1734
1735 @Override
1736 public void bind(Item note) {
1737 binding.message.setText(note.el.getContent());
1738
1739 String type = note.el.getAttribute("type");
1740 if (type != null && type.equals("error")) {
1741 binding.errorIcon.setVisibility(View.VISIBLE);
1742 }
1743 }
1744 }
1745
1746 class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
1747 public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
1748
1749 @Override
1750 public void bind(Item item) {
1751 Field field = (Field) item;
1752 setTextOrHide(binding.label, field.getLabel());
1753 setTextOrHide(binding.desc, field.getDesc());
1754
1755 Element media = field.el.findChild("media", "urn:xmpp:media-element");
1756 if (media == null) {
1757 binding.mediaImage.setVisibility(View.GONE);
1758 } else {
1759 final LruCache<String, Drawable> cache = xmppConnectionService.getDrawableCache();
1760 final HttpConnectionManager httpManager = xmppConnectionService.getHttpConnectionManager();
1761 for (Element uriEl : media.getChildren()) {
1762 if (!"uri".equals(uriEl.getName())) continue;
1763 if (!"urn:xmpp:media-element".equals(uriEl.getNamespace())) continue;
1764 String mimeType = uriEl.getAttribute("type");
1765 String uriS = uriEl.getContent();
1766 if (mimeType == null || uriS == null) continue;
1767 Uri uri = Uri.parse(uriS);
1768 if (mimeType.startsWith("image/") && "https".equals(uri.getScheme())) {
1769 final Drawable d = cache.get(uri.toString());
1770 if (d == null) {
1771 int size = (int)(xmppConnectionService.getResources().getDisplayMetrics().density * 288);
1772 Message dummy = new Message(Conversation.this, uri.toString(), Message.ENCRYPTION_NONE);
1773 dummy.setFileParams(new Message.FileParams(uri.toString()));
1774 httpManager.createNewDownloadConnection(dummy, true, (file) -> {
1775 if (file == null) {
1776 dummy.getTransferable().start();
1777 } else {
1778 try {
1779 xmppConnectionService.getFileBackend().getThumbnail(file, xmppConnectionService.getResources(), size, false, uri.toString());
1780 } catch (final Exception e) { }
1781 }
1782 });
1783 } else {
1784 binding.mediaImage.setImageDrawable(d);
1785 binding.mediaImage.setVisibility(View.VISIBLE);
1786 }
1787 }
1788 }
1789 }
1790
1791 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1792 String datatype = validate == null ? null : validate.getAttribute("datatype");
1793
1794 ArrayAdapter<Option> values = new ArrayAdapter<>(binding.getRoot().getContext(), R.layout.simple_list_item);
1795 for (Element el : field.el.getChildren()) {
1796 if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
1797 values.add(new Option(el.getContent(), formatValue(datatype, el.getContent(), false)));
1798 }
1799 }
1800 binding.values.setAdapter(values);
1801 Util.justifyListViewHeightBasedOnChildren(binding.values);
1802
1803 if (field.getType().equals(Optional.of("jid-single")) || field.getType().equals(Optional.of("jid-multi"))) {
1804 binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
1805 new FixedURLSpan("xmpp:" + Jid.ofEscaped(values.getItem(pos).getValue()).toEscapedString(), account).onClick(binding.values);
1806 });
1807 } else if ("xs:anyURI".equals(datatype)) {
1808 binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
1809 new FixedURLSpan(values.getItem(pos).getValue(), account).onClick(binding.values);
1810 });
1811 } else if ("html:tel".equals(datatype)) {
1812 binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
1813 try {
1814 new FixedURLSpan("tel:" + PhoneNumberUtilWrapper.normalize(binding.getRoot().getContext(), values.getItem(pos).getValue()), account).onClick(binding.values);
1815 } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) { }
1816 });
1817 }
1818
1819 binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
1820 if (ShareUtil.copyTextToClipboard(binding.getRoot().getContext(), values.getItem(pos).getValue(), R.string.message)) {
1821 Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
1822 }
1823 return true;
1824 });
1825 }
1826 }
1827
1828 class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
1829 public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
1830
1831 @Override
1832 public void bind(Item item) {
1833 Cell cell = (Cell) item;
1834
1835 if (cell.el == null) {
1836 binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
1837 setTextOrHide(binding.text, cell.reported.getLabel());
1838 } else {
1839 Element validate = cell.reported.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1840 String datatype = validate == null ? null : validate.getAttribute("datatype");
1841 String value = formatValue(datatype, cell.el.findChildContent("value", "jabber:x:data"), true);
1842 SpannableStringBuilder text = new SpannableStringBuilder(value == null ? "" : value);
1843 if (cell.reported.getType().equals(Optional.of("jid-single"))) {
1844 text.setSpan(new FixedURLSpan("xmpp:" + Jid.ofEscaped(text.toString()).toEscapedString(), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1845 } else if ("xs:anyURI".equals(datatype)) {
1846 text.setSpan(new FixedURLSpan(text.toString(), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1847 } else if ("html:tel".equals(datatype)) {
1848 try {
1849 text.setSpan(new FixedURLSpan("tel:" + PhoneNumberUtilWrapper.normalize(binding.getRoot().getContext(), text.toString()), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
1850 } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) { }
1851 }
1852
1853 binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
1854 binding.text.setText(text);
1855
1856 BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance();
1857 method.setOnLinkLongClickListener((tv, url) -> {
1858 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
1859 ShareUtil.copyLinkToClipboard(binding.getRoot().getContext(), url);
1860 return true;
1861 });
1862 binding.text.setMovementMethod(method);
1863 }
1864 }
1865 }
1866
1867 class ItemCardViewHolder extends ViewHolder<CommandItemCardBinding> {
1868 public ItemCardViewHolder(CommandItemCardBinding binding) { super(binding); }
1869
1870 @Override
1871 public void bind(Item item) {
1872 binding.fields.removeAllViews();
1873
1874 for (Field field : reported) {
1875 CommandResultFieldBinding row = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.command_result_field, binding.fields, false);
1876 GridLayout.LayoutParams param = new GridLayout.LayoutParams();
1877 param.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f);
1878 param.width = 0;
1879 row.getRoot().setLayoutParams(param);
1880 binding.fields.addView(row.getRoot());
1881 for (Element el : item.el.getChildren()) {
1882 if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data") && el.getAttribute("var") != null && el.getAttribute("var").equals(field.getVar())) {
1883 for (String label : field.getLabel().asSet()) {
1884 el.setAttribute("label", label);
1885 }
1886 for (String desc : field.getDesc().asSet()) {
1887 el.setAttribute("desc", desc);
1888 }
1889 for (String type : field.getType().asSet()) {
1890 el.setAttribute("type", type);
1891 }
1892 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1893 if (validate != null) el.addChild(validate);
1894 new ResultFieldViewHolder(row).bind(new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), -1));
1895 }
1896 }
1897 }
1898 }
1899 }
1900
1901 class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
1902 public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
1903 super(binding);
1904 binding.row.setOnClickListener((v) -> {
1905 binding.checkbox.toggle();
1906 });
1907 binding.checkbox.setOnCheckedChangeListener(this);
1908 }
1909 protected Element mValue = null;
1910
1911 @Override
1912 public void bind(Item item) {
1913 Field field = (Field) item;
1914 binding.label.setText(field.getLabel().or(""));
1915 setTextOrHide(binding.desc, field.getDesc());
1916 mValue = field.getValue();
1917 binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
1918 }
1919
1920 @Override
1921 public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
1922 if (mValue == null) return;
1923
1924 mValue.setContent(isChecked ? "true" : "false");
1925 }
1926 }
1927
1928 class SearchListFieldViewHolder extends ViewHolder<CommandSearchListFieldBinding> implements TextWatcher {
1929 public SearchListFieldViewHolder(CommandSearchListFieldBinding binding) {
1930 super(binding);
1931 binding.search.addTextChangedListener(this);
1932 }
1933 protected Element mValue = null;
1934 List<Option> options = new ArrayList<>();
1935 protected ArrayAdapter<Option> adapter;
1936 protected boolean open;
1937
1938 @Override
1939 public void bind(Item item) {
1940 Field field = (Field) item;
1941 setTextOrHide(binding.label, field.getLabel());
1942 setTextOrHide(binding.desc, field.getDesc());
1943
1944 if (field.error != null) {
1945 binding.desc.setVisibility(View.VISIBLE);
1946 binding.desc.setText(field.error);
1947 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
1948 } else {
1949 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
1950 }
1951
1952 mValue = field.getValue();
1953
1954 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
1955 open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
1956 setupInputType(field.el, binding.search, null);
1957
1958 options = field.getOptions();
1959 binding.list.setOnItemClickListener((parent, view, position, id) -> {
1960 mValue.setContent(adapter.getItem(binding.list.getCheckedItemPosition()).getValue());
1961 if (open) binding.search.setText(mValue.getContent());
1962 });
1963 search("");
1964 }
1965
1966 @Override
1967 public void afterTextChanged(Editable s) {
1968 if (open) mValue.setContent(s.toString());
1969 search(s.toString());
1970 }
1971
1972 @Override
1973 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
1974
1975 @Override
1976 public void onTextChanged(CharSequence s, int start, int count, int after) { }
1977
1978 protected void search(String s) {
1979 List<Option> filteredOptions;
1980 final String q = s.replaceAll("\\W", "").toLowerCase();
1981 if (q == null || q.equals("")) {
1982 filteredOptions = options;
1983 } else {
1984 filteredOptions = options.stream().filter(o -> o.toString().replaceAll("\\W", "").toLowerCase().contains(q)).collect(Collectors.toList());
1985 }
1986 adapter = new ArrayAdapter(binding.getRoot().getContext(), R.layout.simple_list_item, filteredOptions);
1987 binding.list.setAdapter(adapter);
1988
1989 int checkedPos = filteredOptions.indexOf(new Option(mValue.getContent(), ""));
1990 if (checkedPos >= 0) binding.list.setItemChecked(checkedPos, true);
1991 }
1992 }
1993
1994 class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
1995 public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
1996 super(binding);
1997 binding.open.addTextChangedListener(this);
1998 options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
1999 @Override
2000 public View getView(int position, View convertView, ViewGroup parent) {
2001 CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
2002 v.setId(position);
2003 v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
2004 v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
2005 return v;
2006 }
2007 };
2008 }
2009 protected Element mValue = null;
2010 protected ArrayAdapter<Option> options;
2011
2012 @Override
2013 public void bind(Item item) {
2014 Field field = (Field) item;
2015 setTextOrHide(binding.label, field.getLabel());
2016 setTextOrHide(binding.desc, field.getDesc());
2017
2018 if (field.error != null) {
2019 binding.desc.setVisibility(View.VISIBLE);
2020 binding.desc.setText(field.error);
2021 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
2022 } else {
2023 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
2024 }
2025
2026 mValue = field.getValue();
2027
2028 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2029 binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
2030 binding.open.setText(mValue.getContent());
2031 setupInputType(field.el, binding.open, null);
2032
2033 options.clear();
2034 List<Option> theOptions = field.getOptions();
2035 options.addAll(theOptions);
2036
2037 float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
2038 TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
2039 float maxColumnWidth = theOptions.stream().map((x) ->
2040 StaticLayout.getDesiredWidth(x.toString(), paint)
2041 ).max(Float::compare).orElse(new Float(0.0));
2042 if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
2043 binding.radios.setNumColumns(theOptions.size());
2044 } else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
2045 binding.radios.setNumColumns(theOptions.size() / 2);
2046 } else {
2047 binding.radios.setNumColumns(1);
2048 }
2049 binding.radios.setAdapter(options);
2050 }
2051
2052 @Override
2053 public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
2054 if (mValue == null) return;
2055
2056 if (isChecked) {
2057 mValue.setContent(options.getItem(radio.getId()).getValue());
2058 binding.open.setText(mValue.getContent());
2059 }
2060 options.notifyDataSetChanged();
2061 }
2062
2063 @Override
2064 public void afterTextChanged(Editable s) {
2065 if (mValue == null) return;
2066
2067 mValue.setContent(s.toString());
2068 options.notifyDataSetChanged();
2069 }
2070
2071 @Override
2072 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
2073
2074 @Override
2075 public void onTextChanged(CharSequence s, int start, int count, int after) { }
2076 }
2077
2078 class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
2079 public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
2080 super(binding);
2081 binding.spinner.setOnItemSelectedListener(this);
2082 }
2083 protected Element mValue = null;
2084
2085 @Override
2086 public void bind(Item item) {
2087 Field field = (Field) item;
2088 setTextOrHide(binding.label, field.getLabel());
2089 binding.spinner.setPrompt(field.getLabel().or(""));
2090 setTextOrHide(binding.desc, field.getDesc());
2091
2092 mValue = field.getValue();
2093
2094 ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
2095 options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
2096 options.addAll(field.getOptions());
2097
2098 binding.spinner.setAdapter(options);
2099 binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
2100 }
2101
2102 @Override
2103 public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
2104 Option o = (Option) parent.getItemAtPosition(pos);
2105 if (mValue == null) return;
2106
2107 mValue.setContent(o == null ? "" : o.getValue());
2108 }
2109
2110 @Override
2111 public void onNothingSelected(AdapterView<?> parent) {
2112 mValue.setContent("");
2113 }
2114 }
2115
2116 class ButtonGridFieldViewHolder extends ViewHolder<CommandButtonGridFieldBinding> {
2117 public ButtonGridFieldViewHolder(CommandButtonGridFieldBinding binding) {
2118 super(binding);
2119 options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.button_grid_item) {
2120 @Override
2121 public View getView(int position, View convertView, ViewGroup parent) {
2122 Button v = (Button) super.getView(position, convertView, parent);
2123 v.setOnClickListener((view) -> {
2124 mValue.setContent(getItem(position).getValue());
2125 execute();
2126 loading = true;
2127 });
2128
2129 final SVG icon = getItem(position).getIcon();
2130 if (icon != null) {
2131 v.post(() -> {
2132 if (v.getHeight() == 0) return;
2133 icon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
2134 Bitmap bitmap = Bitmap.createBitmap(v.getHeight(), v.getHeight(), Bitmap.Config.ARGB_8888);
2135 Canvas bmcanvas = new Canvas(bitmap);
2136 icon.renderToCanvas(bmcanvas);
2137 v.setCompoundDrawablesRelativeWithIntrinsicBounds(new BitmapDrawable(bitmap), null, null, null);
2138 });
2139 }
2140
2141 return v;
2142 }
2143 };
2144 }
2145 protected Element mValue = null;
2146 protected ArrayAdapter<Option> options;
2147 protected Option defaultOption = null;
2148
2149 @Override
2150 public void bind(Item item) {
2151 Field field = (Field) item;
2152 setTextOrHide(binding.label, field.getLabel());
2153 setTextOrHide(binding.desc, field.getDesc());
2154
2155 if (field.error != null) {
2156 binding.desc.setVisibility(View.VISIBLE);
2157 binding.desc.setText(field.error);
2158 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
2159 } else {
2160 binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
2161 }
2162
2163 mValue = field.getValue();
2164
2165 Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2166 binding.openButton.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
2167 binding.openButton.setOnClickListener((view) -> {
2168 AlertDialog.Builder builder = new AlertDialog.Builder(binding.getRoot().getContext());
2169 DialogQuickeditBinding dialogBinding = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.dialog_quickedit, null, false);
2170 builder.setPositiveButton(R.string.action_execute, null);
2171 if (field.getDesc().isPresent()) {
2172 dialogBinding.inputLayout.setHint(field.getDesc().get());
2173 }
2174 dialogBinding.inputEditText.requestFocus();
2175 dialogBinding.inputEditText.getText().append(mValue.getContent());
2176 builder.setView(dialogBinding.getRoot());
2177 builder.setNegativeButton(R.string.cancel, null);
2178 final AlertDialog dialog = builder.create();
2179 dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(dialogBinding.inputEditText));
2180 dialog.show();
2181 View.OnClickListener clickListener = v -> {
2182 String value = dialogBinding.inputEditText.getText().toString();
2183 mValue.setContent(value);
2184 SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2185 dialog.dismiss();
2186 execute();
2187 loading = true;
2188 };
2189 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
2190 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
2191 SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2192 dialog.dismiss();
2193 }));
2194 dialog.setCanceledOnTouchOutside(false);
2195 dialog.setOnDismissListener(dialog1 -> {
2196 SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
2197 });
2198 });
2199
2200 options.clear();
2201 List<Option> theOptions = field.getType().equals(Optional.of("boolean")) ? new ArrayList<>(List.of(new Option("false", binding.getRoot().getContext().getString(R.string.no)), new Option("true", binding.getRoot().getContext().getString(R.string.yes)))) : field.getOptions();
2202
2203 defaultOption = null;
2204 for (Option option : theOptions) {
2205 if (option.getValue().equals(mValue.getContent())) {
2206 defaultOption = option;
2207 break;
2208 }
2209 }
2210 if (defaultOption == null && !mValue.getContent().equals("")) {
2211 // Synthesize default option for custom value
2212 defaultOption = new Option(mValue.getContent(), mValue.getContent());
2213 }
2214 if (defaultOption == null) {
2215 binding.defaultButton.setVisibility(View.GONE);
2216 } else {
2217 theOptions.remove(defaultOption);
2218 binding.defaultButton.setVisibility(View.VISIBLE);
2219
2220 final SVG defaultIcon = defaultOption.getIcon();
2221 if (defaultIcon != null) {
2222 defaultIcon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
2223 DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
2224 Bitmap bitmap = Bitmap.createBitmap((int)(display.heightPixels*display.density/4), (int)(display.heightPixels*display.density/4), Bitmap.Config.ARGB_8888);
2225 bitmap.setDensity(display.densityDpi);
2226 Canvas bmcanvas = new Canvas(bitmap);
2227 defaultIcon.renderToCanvas(bmcanvas);
2228 binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
2229 }
2230
2231 binding.defaultButton.setText(defaultOption.toString());
2232 binding.defaultButton.setOnClickListener((view) -> {
2233 mValue.setContent(defaultOption.getValue());
2234 execute();
2235 loading = true;
2236 });
2237 }
2238
2239 options.addAll(theOptions);
2240 binding.buttons.setAdapter(options);
2241 }
2242 }
2243
2244 class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
2245 public TextFieldViewHolder(CommandTextFieldBinding binding) {
2246 super(binding);
2247 binding.textinput.addTextChangedListener(this);
2248 }
2249 protected Field field = null;
2250
2251 @Override
2252 public void bind(Item item) {
2253 field = (Field) item;
2254 binding.textinputLayout.setHint(field.getLabel().or(""));
2255
2256 binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
2257 for (String desc : field.getDesc().asSet()) {
2258 binding.textinputLayout.setHelperText(desc);
2259 }
2260
2261 binding.textinputLayout.setErrorEnabled(field.error != null);
2262 if (field.error != null) binding.textinputLayout.setError(field.error);
2263
2264 binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
2265 String suffixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/suffix-label");
2266 if (suffixLabel == null) {
2267 binding.textinputLayout.setSuffixText("");
2268 } else {
2269 binding.textinputLayout.setSuffixText(suffixLabel);
2270 binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END);
2271 }
2272
2273 String prefixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/prefix-label");
2274 binding.textinputLayout.setPrefixText(prefixLabel == null ? "" : prefixLabel);
2275
2276 binding.textinput.setText(String.join("\n", field.getValues()));
2277 setupInputType(field.el, binding.textinput, binding.textinputLayout);
2278 }
2279
2280 @Override
2281 public void afterTextChanged(Editable s) {
2282 if (field == null) return;
2283
2284 field.setValues(List.of(s.toString().split("\n")));
2285 }
2286
2287 @Override
2288 public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
2289
2290 @Override
2291 public void onTextChanged(CharSequence s, int start, int count, int after) { }
2292 }
2293
2294 class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
2295 public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
2296 protected String boundUrl = "";
2297
2298 @Override
2299 public void bind(Item oob) {
2300 setTextOrHide(binding.desc, Optional.fromNullable(oob.el.findChildContent("desc", "jabber:x:oob")));
2301 binding.webview.getSettings().setJavaScriptEnabled(true);
2302 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");
2303 binding.webview.getSettings().setDatabaseEnabled(true);
2304 binding.webview.getSettings().setDomStorageEnabled(true);
2305 binding.webview.setWebChromeClient(new WebChromeClient() {
2306 @Override
2307 public void onProgressChanged(WebView view, int newProgress) {
2308 binding.progressbar.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
2309 binding.progressbar.setProgress(newProgress);
2310 }
2311 });
2312 binding.webview.setWebViewClient(new WebViewClient() {
2313 @Override
2314 public void onPageFinished(WebView view, String url) {
2315 super.onPageFinished(view, url);
2316 mTitle = view.getTitle();
2317 ConversationPagerAdapter.this.notifyDataSetChanged();
2318 }
2319 });
2320 final String url = oob.el.findChildContent("url", "jabber:x:oob");
2321 if (!boundUrl.equals(url)) {
2322 binding.webview.addJavascriptInterface(new JsObject(), "xmpp_xep0050");
2323 binding.webview.loadUrl(url);
2324 boundUrl = url;
2325 }
2326 }
2327
2328 class JsObject {
2329 @JavascriptInterface
2330 public void execute() { execute("execute"); }
2331
2332 @JavascriptInterface
2333 public void execute(String action) {
2334 getView().post(() -> {
2335 actionToWebview = null;
2336 if(CommandSession.this.execute(action)) {
2337 removeSession(CommandSession.this);
2338 }
2339 });
2340 }
2341
2342 @JavascriptInterface
2343 public void preventDefault() {
2344 actionToWebview = binding.webview;
2345 }
2346 }
2347 }
2348
2349 class ProgressBarViewHolder extends ViewHolder<CommandProgressBarBinding> {
2350 public ProgressBarViewHolder(CommandProgressBarBinding binding) { super(binding); }
2351
2352 @Override
2353 public void bind(Item item) {
2354 binding.text.setVisibility(loadingHasBeenLong ? View.VISIBLE : View.GONE);
2355 }
2356 }
2357
2358 class Item {
2359 protected Element el;
2360 protected int viewType;
2361 protected String error = null;
2362
2363 Item(Element el, int viewType) {
2364 this.el = el;
2365 this.viewType = viewType;
2366 }
2367
2368 public boolean validate() {
2369 error = null;
2370 return true;
2371 }
2372 }
2373
2374 class Field extends Item {
2375 Field(eu.siacs.conversations.xmpp.forms.Field el, int viewType) { super(el, viewType); }
2376
2377 @Override
2378 public boolean validate() {
2379 if (!super.validate()) return false;
2380 if (el.findChild("required", "jabber:x:data") == null) return true;
2381 if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
2382
2383 error = "this value is required";
2384 return false;
2385 }
2386
2387 public String getVar() {
2388 return el.getAttribute("var");
2389 }
2390
2391 public Optional<String> getType() {
2392 return Optional.fromNullable(el.getAttribute("type"));
2393 }
2394
2395 public Optional<String> getLabel() {
2396 String label = el.getAttribute("label");
2397 if (label == null) label = getVar();
2398 return Optional.fromNullable(label);
2399 }
2400
2401 public Optional<String> getDesc() {
2402 return Optional.fromNullable(el.findChildContent("desc", "jabber:x:data"));
2403 }
2404
2405 public Element getValue() {
2406 Element value = el.findChild("value", "jabber:x:data");
2407 if (value == null) {
2408 value = el.addChild("value", "jabber:x:data");
2409 }
2410 return value;
2411 }
2412
2413 public void setValues(List<String> values) {
2414 for(Element child : el.getChildren()) {
2415 if ("value".equals(child.getName()) && "jabber:x:data".equals(child.getNamespace())) {
2416 el.removeChild(child);
2417 }
2418 }
2419
2420 for (String value : values) {
2421 el.addChild("value", "jabber:x:data").setContent(value);
2422 }
2423 }
2424
2425 public List<String> getValues() {
2426 List<String> values = new ArrayList<>();
2427 for(Element child : el.getChildren()) {
2428 if ("value".equals(child.getName()) && "jabber:x:data".equals(child.getNamespace())) {
2429 values.add(child.getContent());
2430 }
2431 }
2432 return values;
2433 }
2434
2435 public List<Option> getOptions() {
2436 return Option.forField(el);
2437 }
2438 }
2439
2440 class Cell extends Item {
2441 protected Field reported;
2442
2443 Cell(Field reported, Element item) {
2444 super(item, TYPE_RESULT_CELL);
2445 this.reported = reported;
2446 }
2447 }
2448
2449 protected Field mkField(Element el) {
2450 int viewType = -1;
2451
2452 String formType = responseElement.getAttribute("type");
2453 if (formType != null) {
2454 String fieldType = el.getAttribute("type");
2455 if (fieldType == null) fieldType = "text-single";
2456
2457 if (formType.equals("result") || fieldType.equals("fixed")) {
2458 viewType = TYPE_RESULT_FIELD;
2459 } else if (formType.equals("form")) {
2460 if (fieldType.equals("boolean")) {
2461 if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2462 viewType = TYPE_BUTTON_GRID_FIELD;
2463 } else {
2464 viewType = TYPE_CHECKBOX_FIELD;
2465 }
2466 } else if (fieldType.equals("list-single")) {
2467 Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
2468 if (Option.forField(el).size() > 9) {
2469 viewType = TYPE_SEARCH_LIST_FIELD;
2470 } else if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
2471 viewType = TYPE_BUTTON_GRID_FIELD;
2472 } else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
2473 viewType = TYPE_RADIO_EDIT_FIELD;
2474 } else {
2475 viewType = TYPE_SPINNER_FIELD;
2476 }
2477 } else {
2478 viewType = TYPE_TEXT_FIELD;
2479 }
2480 }
2481
2482 return new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), viewType);
2483 }
2484
2485 return null;
2486 }
2487
2488 protected Item mkItem(Element el, int pos) {
2489 int viewType = -1;
2490
2491 if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
2492 if (el.getName().equals("note")) {
2493 viewType = TYPE_NOTE;
2494 } else if (el.getNamespace().equals("jabber:x:oob")) {
2495 viewType = TYPE_WEB;
2496 } else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
2497 viewType = TYPE_NOTE;
2498 } else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
2499 Field field = mkField(el);
2500 if (field != null) {
2501 items.put(pos, field);
2502 return field;
2503 }
2504 }
2505 } else if (response != null) {
2506 viewType = TYPE_ERROR;
2507 }
2508
2509 Item item = new Item(el, viewType);
2510 items.put(pos, item);
2511 return item;
2512 }
2513
2514 class ActionsAdapter extends ArrayAdapter<Pair<String, String>> {
2515 protected Context ctx;
2516
2517 public ActionsAdapter(Context ctx) {
2518 super(ctx, R.layout.simple_list_item);
2519 this.ctx = ctx;
2520 }
2521
2522 @Override
2523 public View getView(int position, View convertView, ViewGroup parent) {
2524 View v = super.getView(position, convertView, parent);
2525 TextView tv = (TextView) v.findViewById(android.R.id.text1);
2526 tv.setGravity(Gravity.CENTER);
2527 tv.setText(getItem(position).second);
2528 int resId = ctx.getResources().getIdentifier("action_" + getItem(position).first, "string" , ctx.getPackageName());
2529 if (resId != 0 && getItem(position).second.equals(getItem(position).first)) tv.setText(ctx.getResources().getString(resId));
2530 tv.setTextColor(ContextCompat.getColor(ctx, R.color.white));
2531 tv.setBackgroundColor(UIHelper.getColorForName(getItem(position).first));
2532 return v;
2533 }
2534
2535 public int getPosition(String s) {
2536 for(int i = 0; i < getCount(); i++) {
2537 if (getItem(i).first.equals(s)) return i;
2538 }
2539 return -1;
2540 }
2541
2542 public int countExceptCancel() {
2543 int count = 0;
2544 for(int i = 0; i < getCount(); i++) {
2545 if (!getItem(i).first.equals("cancel")) count++;
2546 }
2547 return count;
2548 }
2549
2550 public void clearExceptCancel() {
2551 Pair<String,String> cancelItem = null;
2552 for(int i = 0; i < getCount(); i++) {
2553 if (getItem(i).first.equals("cancel")) cancelItem = getItem(i);
2554 }
2555 clear();
2556 if (cancelItem != null) add(cancelItem);
2557 }
2558 }
2559
2560 final int TYPE_ERROR = 1;
2561 final int TYPE_NOTE = 2;
2562 final int TYPE_WEB = 3;
2563 final int TYPE_RESULT_FIELD = 4;
2564 final int TYPE_TEXT_FIELD = 5;
2565 final int TYPE_CHECKBOX_FIELD = 6;
2566 final int TYPE_SPINNER_FIELD = 7;
2567 final int TYPE_RADIO_EDIT_FIELD = 8;
2568 final int TYPE_RESULT_CELL = 9;
2569 final int TYPE_PROGRESSBAR = 10;
2570 final int TYPE_SEARCH_LIST_FIELD = 11;
2571 final int TYPE_ITEM_CARD = 12;
2572 final int TYPE_BUTTON_GRID_FIELD = 13;
2573
2574 protected boolean executing = false;
2575 protected boolean loading = false;
2576 protected boolean loadingHasBeenLong = false;
2577 protected Timer loadingTimer = new Timer();
2578 protected String mTitle;
2579 protected String mNode;
2580 protected CommandPageBinding mBinding = null;
2581 protected IqPacket response = null;
2582 protected Element responseElement = null;
2583 protected List<Field> reported = null;
2584 protected SparseArray<Item> items = new SparseArray<>();
2585 protected XmppConnectionService xmppConnectionService;
2586 protected ActionsAdapter actionsAdapter;
2587 protected GridLayoutManager layoutManager;
2588 protected WebView actionToWebview = null;
2589 protected int fillableFieldCount = 0;
2590 protected IqPacket pendingResponsePacket = null;
2591
2592 CommandSession(String title, String node, XmppConnectionService xmppConnectionService) {
2593 loading();
2594 mTitle = title;
2595 mNode = node;
2596 this.xmppConnectionService = xmppConnectionService;
2597 if (mPager != null) setupLayoutManager();
2598 actionsAdapter = new ActionsAdapter(xmppConnectionService);
2599 actionsAdapter.registerDataSetObserver(new DataSetObserver() {
2600 @Override
2601 public void onChanged() {
2602 if (mBinding == null) return;
2603
2604 mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
2605 }
2606
2607 @Override
2608 public void onInvalidated() {}
2609 });
2610 }
2611
2612 public String getTitle() {
2613 return mTitle;
2614 }
2615
2616 public String getNode() {
2617 return mNode;
2618 }
2619
2620 public void updateWithResponse(final IqPacket iq) {
2621 if (getView() != null && getView().isAttachedToWindow()) {
2622 getView().post(() -> updateWithResponseUiThread(iq));
2623 } else {
2624 pendingResponsePacket = iq;
2625 }
2626 }
2627
2628 protected void updateWithResponseUiThread(final IqPacket iq) {
2629 this.loadingTimer.cancel();
2630 this.loadingTimer = new Timer();
2631 this.executing = false;
2632 this.loading = false;
2633 this.loadingHasBeenLong = false;
2634 this.responseElement = null;
2635 this.fillableFieldCount = 0;
2636 this.reported = null;
2637 this.response = iq;
2638 this.items.clear();
2639 this.actionsAdapter.clear();
2640 layoutManager.setSpanCount(1);
2641
2642 boolean actionsCleared = false;
2643 Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
2644 if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
2645 if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) {
2646 xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true);
2647 }
2648
2649 if (xmppConnectionService.isOnboarding() && mNode.equals("jabber:iq:register") && !"canceled".equals(command.getAttribute("status")) && xmppConnectionService.getPreferences().contains("onboarding_action")) {
2650 xmppConnectionService.getPreferences().edit().putBoolean("onboarding_continued", true).commit();
2651 }
2652
2653 Element actions = command.findChild("actions", "http://jabber.org/protocol/commands");
2654 if (actions != null) {
2655 for (Element action : actions.getChildren()) {
2656 if (!"http://jabber.org/protocol/commands".equals(action.getNamespace())) continue;
2657 if ("execute".equals(action.getName())) continue;
2658
2659 actionsAdapter.add(Pair.create(action.getName(), action.getName()));
2660 }
2661 }
2662
2663 for (Element el : command.getChildren()) {
2664 if ("x".equals(el.getName()) && "jabber:x:data".equals(el.getNamespace())) {
2665 Data form = Data.parse(el);
2666 String title = form.getTitle();
2667 if (title != null) {
2668 mTitle = title;
2669 ConversationPagerAdapter.this.notifyDataSetChanged();
2670 }
2671
2672 if ("result".equals(el.getAttribute("type")) || "form".equals(el.getAttribute("type"))) {
2673 this.responseElement = el;
2674 setupReported(el.findChild("reported", "jabber:x:data"));
2675 if (mBinding != null) mBinding.form.setLayoutManager(setupLayoutManager());
2676 }
2677
2678 eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
2679 if (actionList != null) {
2680 actionsAdapter.clear();
2681
2682 for (Option action : actionList.getOptions()) {
2683 actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
2684 }
2685 }
2686
2687 String fillableFieldType = null;
2688 String fillableFieldValue = null;
2689 for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
2690 if ((field.getType() == null || (!field.getType().equals("hidden") && !field.getType().equals("fixed"))) && field.getFieldName() != null && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
2691 fillableFieldType = field.getType();
2692 fillableFieldValue = field.getValue();
2693 fillableFieldCount++;
2694 }
2695 }
2696
2697 if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 2 && fillableFieldType != null && (fillableFieldType.equals("list-single") || (fillableFieldType.equals("boolean") && fillableFieldValue == null))) {
2698 actionsCleared = true;
2699 actionsAdapter.clearExceptCancel();
2700 }
2701 break;
2702 }
2703 if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
2704 String url = el.findChildContent("url", "jabber:x:oob");
2705 if (url != null) {
2706 String scheme = Uri.parse(url).getScheme();
2707 if (scheme.equals("http") || scheme.equals("https")) {
2708 this.responseElement = el;
2709 break;
2710 }
2711 if (scheme.equals("xmpp")) {
2712 final Intent intent = new Intent(getView().getContext(), UriHandlerActivity.class);
2713 intent.setAction(Intent.ACTION_VIEW);
2714 intent.setData(Uri.parse(url));
2715 getView().getContext().startActivity(intent);
2716 break;
2717 }
2718 }
2719 }
2720 if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
2721 this.responseElement = el;
2722 break;
2723 }
2724 }
2725
2726 if (responseElement == null && command.getAttribute("status") != null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
2727 if ("jabber:iq:register".equals(mNode) && "canceled".equals(command.getAttribute("status"))) {
2728 if (xmppConnectionService.isOnboarding()) {
2729 if (xmppConnectionService.getPreferences().contains("onboarding_action")) {
2730 xmppConnectionService.deleteAccount(getAccount());
2731 } else {
2732 if (xmppConnectionService.getPreferences().getBoolean("onboarding_continued", false)) {
2733 removeSession(this);
2734 return;
2735 } else {
2736 xmppConnectionService.getPreferences().edit().putString("onboarding_action", "cancel").commit();
2737 xmppConnectionService.deleteAccount(getAccount());
2738 }
2739 }
2740 }
2741 xmppConnectionService.archiveConversation(Conversation.this);
2742 }
2743
2744 removeSession(this);
2745 return;
2746 }
2747
2748 if ("executing".equals(command.getAttribute("status")) && actionsAdapter.countExceptCancel() < 1 && !actionsCleared) {
2749 // No actions have been given, but we are not done?
2750 // This is probably a spec violation, but we should do *something*
2751 actionsAdapter.add(Pair.create("execute", "execute"));
2752 }
2753
2754 if (!actionsAdapter.isEmpty() || fillableFieldCount > 0) {
2755 if ("completed".equals(command.getAttribute("status")) || "canceled".equals(command.getAttribute("status"))) {
2756 actionsAdapter.add(Pair.create("close", "close"));
2757 } else if (actionsAdapter.getPosition("cancel") < 0 && !xmppConnectionService.isOnboarding()) {
2758 actionsAdapter.insert(Pair.create("cancel", "cancel"), 0);
2759 }
2760 }
2761 }
2762
2763 if (actionsAdapter.isEmpty()) {
2764 actionsAdapter.add(Pair.create("close", "close"));
2765 }
2766
2767 actionsAdapter.sort((x, y) -> {
2768 if (x.first.equals("cancel")) return -1;
2769 if (y.first.equals("cancel")) return 1;
2770 if (x.first.equals("prev") && xmppConnectionService.isOnboarding()) return -1;
2771 if (y.first.equals("prev") && xmppConnectionService.isOnboarding()) return 1;
2772 return 0;
2773 });
2774
2775 Data dataForm = null;
2776 if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
2777 if (mNode.equals("jabber:iq:register") &&
2778 xmppConnectionService.getPreferences().contains("onboarding_action") &&
2779 dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
2780
2781
2782 dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
2783 execute();
2784 }
2785 xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
2786 notifyDataSetChanged();
2787 }
2788
2789 protected void setupReported(Element el) {
2790 if (el == null) {
2791 reported = null;
2792 return;
2793 }
2794
2795 reported = new ArrayList<>();
2796 for (Element fieldEl : el.getChildren()) {
2797 if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
2798 reported.add(mkField(fieldEl));
2799 }
2800 }
2801
2802 @Override
2803 public int getItemCount() {
2804 if (loading) return 1;
2805 if (response == null) return 0;
2806 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
2807 int i = 0;
2808 for (Element el : responseElement.getChildren()) {
2809 if (!el.getNamespace().equals("jabber:x:data")) continue;
2810 if (el.getName().equals("title")) continue;
2811 if (el.getName().equals("field")) {
2812 String type = el.getAttribute("type");
2813 if (type != null && type.equals("hidden")) continue;
2814 if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2815 }
2816
2817 if (el.getName().equals("reported") || el.getName().equals("item")) {
2818 if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2819 if (el.getName().equals("reported")) continue;
2820 i += 1;
2821 } else {
2822 if (reported != null) i += reported.size();
2823 }
2824 continue;
2825 }
2826
2827 i++;
2828 }
2829 return i;
2830 }
2831 return 1;
2832 }
2833
2834 public Item getItem(int position) {
2835 if (loading) return new Item(null, TYPE_PROGRESSBAR);
2836 if (items.get(position) != null) return items.get(position);
2837 if (response == null) return null;
2838
2839 if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
2840 if (responseElement.getNamespace().equals("jabber:x:data")) {
2841 int i = 0;
2842 for (Element el : responseElement.getChildren()) {
2843 if (!el.getNamespace().equals("jabber:x:data")) continue;
2844 if (el.getName().equals("title")) continue;
2845 if (el.getName().equals("field")) {
2846 String type = el.getAttribute("type");
2847 if (type != null && type.equals("hidden")) continue;
2848 if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
2849 }
2850
2851 if (el.getName().equals("reported") || el.getName().equals("item")) {
2852 Cell cell = null;
2853
2854 if (reported != null) {
2855 if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
2856 if (el.getName().equals("reported")) continue;
2857 if (i == position) {
2858 items.put(position, new Item(el, TYPE_ITEM_CARD));
2859 return items.get(position);
2860 }
2861 } else {
2862 if (reported.size() > position - i) {
2863 Field reportedField = reported.get(position - i);
2864 Element itemField = null;
2865 if (el.getName().equals("item")) {
2866 for (Element subel : el.getChildren()) {
2867 if (subel.getAttribute("var").equals(reportedField.getVar())) {
2868 itemField = subel;
2869 break;
2870 }
2871 }
2872 }
2873 cell = new Cell(reportedField, itemField);
2874 } else {
2875 i += reported.size();
2876 continue;
2877 }
2878 }
2879 }
2880
2881 if (cell != null) {
2882 items.put(position, cell);
2883 return cell;
2884 }
2885 }
2886
2887 if (i < position) {
2888 i++;
2889 continue;
2890 }
2891
2892 return mkItem(el, position);
2893 }
2894 }
2895 }
2896
2897 return mkItem(responseElement == null ? response : responseElement, position);
2898 }
2899
2900 @Override
2901 public int getItemViewType(int position) {
2902 return getItem(position).viewType;
2903 }
2904
2905 @Override
2906 public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
2907 switch(viewType) {
2908 case TYPE_ERROR: {
2909 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2910 return new ErrorViewHolder(binding);
2911 }
2912 case TYPE_NOTE: {
2913 CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
2914 return new NoteViewHolder(binding);
2915 }
2916 case TYPE_WEB: {
2917 CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
2918 return new WebViewHolder(binding);
2919 }
2920 case TYPE_RESULT_FIELD: {
2921 CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
2922 return new ResultFieldViewHolder(binding);
2923 }
2924 case TYPE_RESULT_CELL: {
2925 CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
2926 return new ResultCellViewHolder(binding);
2927 }
2928 case TYPE_ITEM_CARD: {
2929 CommandItemCardBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_item_card, container, false);
2930 return new ItemCardViewHolder(binding);
2931 }
2932 case TYPE_CHECKBOX_FIELD: {
2933 CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
2934 return new CheckboxFieldViewHolder(binding);
2935 }
2936 case TYPE_SEARCH_LIST_FIELD: {
2937 CommandSearchListFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_search_list_field, container, false);
2938 return new SearchListFieldViewHolder(binding);
2939 }
2940 case TYPE_RADIO_EDIT_FIELD: {
2941 CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
2942 return new RadioEditFieldViewHolder(binding);
2943 }
2944 case TYPE_SPINNER_FIELD: {
2945 CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
2946 return new SpinnerFieldViewHolder(binding);
2947 }
2948 case TYPE_BUTTON_GRID_FIELD: {
2949 CommandButtonGridFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_button_grid_field, container, false);
2950 return new ButtonGridFieldViewHolder(binding);
2951 }
2952 case TYPE_TEXT_FIELD: {
2953 CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
2954 return new TextFieldViewHolder(binding);
2955 }
2956 case TYPE_PROGRESSBAR: {
2957 CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
2958 return new ProgressBarViewHolder(binding);
2959 }
2960 default:
2961 throw new IllegalArgumentException("Unknown viewType: " + viewType + " based on: " + response);
2962 }
2963 }
2964
2965 @Override
2966 public void onBindViewHolder(ViewHolder viewHolder, int position) {
2967 viewHolder.bind(getItem(position));
2968 }
2969
2970 public View getView() {
2971 if (mBinding == null) return null;
2972 return mBinding.getRoot();
2973 }
2974
2975 public boolean validate() {
2976 int count = getItemCount();
2977 boolean isValid = true;
2978 for (int i = 0; i < count; i++) {
2979 boolean oneIsValid = getItem(i).validate();
2980 isValid = isValid && oneIsValid;
2981 }
2982 notifyDataSetChanged();
2983 return isValid;
2984 }
2985
2986 public boolean execute() {
2987 return execute("execute");
2988 }
2989
2990 public boolean execute(int actionPosition) {
2991 return execute(actionsAdapter.getItem(actionPosition).first);
2992 }
2993
2994 public synchronized boolean execute(String action) {
2995 if (!"cancel".equals(action) && executing) {
2996 loadingHasBeenLong = true;
2997 notifyDataSetChanged();
2998 return false;
2999 }
3000 if (!action.equals("cancel") && !action.equals("prev") && !validate()) return false;
3001
3002 if (response == null) return true;
3003 Element command = response.findChild("command", "http://jabber.org/protocol/commands");
3004 if (command == null) return true;
3005 String status = command.getAttribute("status");
3006 if (status == null || (!status.equals("executing") && !action.equals("prev"))) return true;
3007
3008 if (actionToWebview != null && !action.equals("cancel")) {
3009 actionToWebview.postWebMessage(new WebMessage("xmpp_xep0050/" + action), Uri.parse("*"));
3010 return false;
3011 }
3012
3013 final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
3014 packet.setTo(response.getFrom());
3015 final Element c = packet.addChild("command", Namespace.COMMANDS);
3016 c.setAttribute("node", mNode);
3017 c.setAttribute("sessionid", command.getAttribute("sessionid"));
3018
3019 String formType = responseElement == null ? null : responseElement.getAttribute("type");
3020 if (!action.equals("cancel") &&
3021 !action.equals("prev") &&
3022 responseElement != null &&
3023 responseElement.getName().equals("x") &&
3024 responseElement.getNamespace().equals("jabber:x:data") &&
3025 formType != null && formType.equals("form")) {
3026
3027 Data form = Data.parse(responseElement);
3028 eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
3029 if (actionList != null) {
3030 actionList.setValue(action);
3031 c.setAttribute("action", "execute");
3032 }
3033
3034 if (mNode.equals("jabber:iq:register") && xmppConnectionService.isOnboarding() && form.getFieldByName("gateway-jid") != null) {
3035 if (form.getValue("gateway-jid") == null) {
3036 xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
3037 } else {
3038 xmppConnectionService.getPreferences().edit().putString("onboarding_action", form.getValue("gateway-jid")).commit();
3039 }
3040 }
3041
3042 responseElement.setAttribute("type", "submit");
3043 Element rsm = responseElement.findChild("set", "http://jabber.org/protocol/rsm");
3044 if (rsm != null) {
3045 Element max = new Element("max", "http://jabber.org/protocol/rsm");
3046 max.setContent("1000");
3047 rsm.addChild(max);
3048 }
3049
3050 c.addChild(responseElement);
3051 }
3052
3053 if (c.getAttribute("action") == null) c.setAttribute("action", action);
3054
3055 executing = true;
3056 xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
3057 updateWithResponse(iq);
3058 });
3059
3060 loading();
3061 return false;
3062 }
3063
3064 public void refresh() {
3065 notifyDataSetChanged();
3066 }
3067
3068 protected void loading() {
3069 View v = getView();
3070 loadingTimer.schedule(new TimerTask() {
3071 @Override
3072 public void run() {
3073 View v2 = getView();
3074 loading = true;
3075
3076 loadingTimer.schedule(new TimerTask() {
3077 @Override
3078 public void run() {
3079 loadingHasBeenLong = true;
3080 if (v == null && v2 == null) return;
3081 (v == null ? v2 : v).post(() -> notifyDataSetChanged());
3082 }
3083 }, 3000);
3084
3085 if (v == null && v2 == null) return;
3086 (v == null ? v2 : v).post(() -> notifyDataSetChanged());
3087 }
3088 }, 500);
3089 }
3090
3091 protected GridLayoutManager setupLayoutManager() {
3092 int spanCount = 1;
3093
3094 Context ctx = mPager == null ? getView().getContext() : mPager.getContext();
3095 if (reported != null) {
3096 float screenWidth = ctx.getResources().getDisplayMetrics().widthPixels;
3097 TextPaint paint = ((TextView) LayoutInflater.from(mPager.getContext()).inflate(R.layout.command_result_cell, null)).getPaint();
3098 float tableHeaderWidth = reported.stream().reduce(
3099 0f,
3100 (total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------") + "\t", paint),
3101 (a, b) -> a + b
3102 );
3103
3104 spanCount = tableHeaderWidth > 0.59 * screenWidth ? 1 : this.reported.size();
3105 }
3106
3107 if (layoutManager != null && layoutManager.getSpanCount() != spanCount) {
3108 items.clear();
3109 notifyDataSetChanged();
3110 }
3111
3112 layoutManager = new GridLayoutManager(ctx, spanCount);
3113 layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
3114 @Override
3115 public int getSpanSize(int position) {
3116 if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
3117 return 1;
3118 }
3119 });
3120 return layoutManager;
3121 }
3122
3123 protected void setBinding(CommandPageBinding b) {
3124 mBinding = b;
3125 // https://stackoverflow.com/a/32350474/8611
3126 mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
3127 @Override
3128 public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
3129 if(rv.getChildCount() > 0) {
3130 int[] location = new int[2];
3131 rv.getLocationOnScreen(location);
3132 View childView = rv.findChildViewUnder(e.getX(), e.getY());
3133 if (childView instanceof ViewGroup) {
3134 childView = findViewAt((ViewGroup) childView, location[0] + e.getX(), location[1] + e.getY());
3135 }
3136 int action = e.getAction();
3137 switch (action) {
3138 case MotionEvent.ACTION_DOWN:
3139 if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(1)) || childView instanceof WebView) {
3140 rv.requestDisallowInterceptTouchEvent(true);
3141 }
3142 case MotionEvent.ACTION_UP:
3143 if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(-11)) || childView instanceof WebView) {
3144 rv.requestDisallowInterceptTouchEvent(true);
3145 }
3146 }
3147 }
3148
3149 return false;
3150 }
3151
3152 @Override
3153 public void onRequestDisallowInterceptTouchEvent(boolean disallow) { }
3154
3155 @Override
3156 public void onTouchEvent(RecyclerView rv, MotionEvent e) { }
3157 });
3158 mBinding.form.setLayoutManager(setupLayoutManager());
3159 mBinding.form.setAdapter(this);
3160 mBinding.actions.setAdapter(actionsAdapter);
3161 mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
3162 if (execute(pos)) {
3163 removeSession(CommandSession.this);
3164 }
3165 });
3166
3167 actionsAdapter.notifyDataSetChanged();
3168
3169 if (pendingResponsePacket != null) {
3170 final IqPacket pending = pendingResponsePacket;
3171 pendingResponsePacket = null;
3172 updateWithResponseUiThread(pending);
3173 }
3174 }
3175
3176 public View inflateUi(Context context, Consumer<ConversationPage> remover) {
3177 CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.command_page, null, false);
3178 setBinding(binding);
3179 return binding.getRoot();
3180 }
3181
3182 // https://stackoverflow.com/a/36037991/8611
3183 private View findViewAt(ViewGroup viewGroup, float x, float y) {
3184 for(int i = 0; i < viewGroup.getChildCount(); i++) {
3185 View child = viewGroup.getChildAt(i);
3186 if (child instanceof ViewGroup && !(child instanceof AbsListView) && !(child instanceof WebView)) {
3187 View foundView = findViewAt((ViewGroup) child, x, y);
3188 if (foundView != null && foundView.isShown()) {
3189 return foundView;
3190 }
3191 } else {
3192 int[] location = new int[2];
3193 child.getLocationOnScreen(location);
3194 Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight());
3195 if (rect.contains((int)x, (int)y)) {
3196 return child;
3197 }
3198 }
3199 }
3200
3201 return null;
3202 }
3203 }
3204 }
3205}