Conversations/src/main/java/eu/siacs/conversations/entities/Conversation.java
2024-02-19 03:58:26 +01:00

3387 lines
143 KiB
Java

package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.telephony.PhoneNumberUtils;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.LruCache;
import android.util.Pair;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebMessage;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.GridLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.databinding.DataBindingUtil;
import androidx.databinding.ViewDataBinding;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.bumptech.glide.Glide;
import com.caverock.androidsvg.SVG;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Optional;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OmemoSetting;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.databinding.CommandButtonGridFieldBinding;
import eu.siacs.conversations.databinding.CommandCheckboxFieldBinding;
import eu.siacs.conversations.databinding.CommandItemCardBinding;
import eu.siacs.conversations.databinding.CommandNoteBinding;
import eu.siacs.conversations.databinding.CommandPageBinding;
import eu.siacs.conversations.databinding.CommandProgressBarBinding;
import eu.siacs.conversations.databinding.CommandRadioEditFieldBinding;
import eu.siacs.conversations.databinding.CommandResultCellBinding;
import eu.siacs.conversations.databinding.CommandResultFieldBinding;
import eu.siacs.conversations.databinding.CommandSearchListFieldBinding;
import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
import eu.siacs.conversations.databinding.CommandTextFieldBinding;
import eu.siacs.conversations.databinding.CommandUnknownBinding;
import eu.siacs.conversations.databinding.CommandWebviewBinding;
import eu.siacs.conversations.databinding.DialogQuickeditBinding;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UriHandlerActivity;
import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Option;
import eu.siacs.conversations.xmpp.mam.MamReference;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import static eu.siacs.conversations.entities.Bookmark.printableValue;
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
public static final String TABLENAME = "conversations";
public static final int STATUS_AVAILABLE = 0;
public static final int STATUS_ARCHIVED = 1;
public static final String NAME = "name";
public static final String ACCOUNT = "accountUuid";
public static final String CONTACT = "contactUuid";
public static final String CONTACTJID = "contactJid";
public static final String STATUS = "status";
public static final String CREATED = "created";
public static final String MODE = "mode";
public static final String ATTRIBUTES = "attributes";
public static final String NEXT_COUNTERPART = "next_counterpart";
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top";
static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
static final String ATTRIBUTE_MODERATED = "moderated";
static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
protected final ArrayList<Message> messages = new ArrayList<>();
protected final ArrayList<Message> historyPartMessages = new ArrayList<>();
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
public AtomicBoolean historyPartLoadedForward = new AtomicBoolean(true);
protected Account account = null;
private String draftMessage;
private final String name;
private final String contactUuid;
private final String accountUuid;
private Jid contactJid;
private int status;
private final long created;
private int mode;
private JSONObject attributes;
private Jid nextCounterpart;
private transient MucOptions mucOptions = null;
private boolean messagesLeftOnServer = true;
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
private String mFirstMamReference = null;
protected Message replyTo = null;
protected int mCurrentTab = -1;
protected ConversationPagerAdapter pagerAdapter = new ConversationPagerAdapter();
private WeakReference<Conversation> parentConversation = null;
public Conversation(final String name, final Account account, final Jid contactJid,
final int mode, Jid nextCounterpart) {
this(java.util.UUID.randomUUID().toString(), name, null, account
.getUuid(), contactJid, System.currentTimeMillis(),
STATUS_AVAILABLE, mode, "", nextCounterpart);
this.account = account;
}
public Conversation(final String uuid, final String name, final String contactUuid,
final String accountUuid, final Jid contactJid, final long created, final int status,
final int mode, final String attributes, Jid nextCounterpart) {
this.uuid = uuid;
this.name = name;
this.contactUuid = contactUuid;
this.accountUuid = accountUuid;
this.contactJid = contactJid;
this.created = created;
this.status = status;
this.mode = mode;
try {
this.attributes = new JSONObject(attributes == null ? "" : attributes);
} catch (JSONException e) {
this.attributes = new JSONObject();
}
this.nextCounterpart = nextCounterpart;
}
public String getContactUuid() {
return contactUuid;
}
public JSONObject getAttributes() {
return attributes;
}
public static Conversation fromCursor(Cursor cursor) {
String counterpart = cursor.getString(cursor.getColumnIndex(NEXT_COUNTERPART));
return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(NAME)),
cursor.getString(cursor.getColumnIndex(CONTACT)),
cursor.getString(cursor.getColumnIndex(ACCOUNT)),
JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
cursor.getLong(cursor.getColumnIndex(CREATED)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(MODE)),
cursor.getString(cursor.getColumnIndex(ATTRIBUTES)),
counterpart == null ? null : JidHelper.parseOrFallbackToInvalid(counterpart));
}
public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
for (int i = messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
if (message.getStatus() <= Message.STATUS_RECEIVED
&& (message.markable || isPrivateAndNonAnonymousMuc)
&& !message.isPrivateMessage()) {
return message;
}
}
return null;
}
private static boolean suitableForOmemoByDefault(final Conversation conversation) {
if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
return false;
}
if (conversation.getContact().isOwnServer()) {
return false;
}
final String contact = conversation.getJid().getDomain().toEscapedString();
final String account = conversation.getAccount().getServer();
if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
return false;
}
return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
}
public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer;
}
public void setHasMessagesLeftOnServer(boolean value) {
this.messagesLeftOnServer = value;
}
public Message getFirstUnreadMessage() {
Message first = null;
synchronized (this.messages) {
for (int i = messages.size() - 1; i >= 0; --i) {
if (messages.get(i).isRead()) {
return first;
} else {
first = messages.get(i);
}
}
}
return first;
}
public String findMostRecentRemoteDisplayableId() {
final boolean multi = mode == Conversation.MODE_MULTI;
synchronized (this.messages) {
for (final Message message : Lists.reverse(this.messages)) {
if (message.getStatus() == Message.STATUS_RECEIVED) {
final String serverMsgId = message.getServerMsgId();
if (serverMsgId != null && multi) {
return serverMsgId;
}
return message.getRemoteMsgId();
}
}
}
return null;
}
public int countFailedDeliveries() {
int count = 0;
synchronized (this.messages) {
for(final Message message : this.messages) {
if (message.getStatus() == Message.STATUS_SEND_FAILED) {
++count;
}
}
}
return count;
}
public Message getLastEditableMessage() {
synchronized (this.messages) {
for (final Message message : Lists.reverse(this.messages)) {
if (message.isEditable()) {
if (message.isGeoUri() || message.getType() != Message.TYPE_TEXT) {
return null;
}
return message;
}
}
}
return null;
}
public Conversation getParentConversation() {
if (parentConversation == null) {
return null;
}
return parentConversation.get();
}
public void setParentConversation(Conversation c) {
this.parentConversation = new WeakReference<>(c);
}
public Message findUnsentMessageWithUuid(String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
final int s = message.getStatus();
if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
return message;
}
}
}
return null;
}
public void findWaitingMessages(OnMessageFound onMessageFound) {
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
for (Message message : this.messages) {
if (message.getStatus() == Message.STATUS_WAITING) {
results.add(message);
}
}
}
for (Message result : results) {
onMessageFound.onMessageFound(result);
}
}
public void findUnreadMessagesAndCalls(OnMessageFound onMessageFound) {
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.isRead()) {
continue;
}
results.add(message);
}
}
for (final Message result : results) {
onMessageFound.onMessageFound(result);
}
}
public Message findMessageWithFileAndUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
final Transferable transferable = message.getTransferable();
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
if (message.getUuid().equals(uuid)
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) {
return message;
}
}
}
return null;
}
public Message findMessageWithUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if (message.getUuid().equals(uuid)) {
return message;
}
}
}
return null;
}
public boolean markAsDeleted(final List<String> uuids) {
boolean deleted = false;
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
synchronized (this.messages) {
for (Message message : this.messages) {
if (uuids.contains(message.getUuid())) {
message.setDeleted(true);
deleted = true;
if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
pgpDecryptionService.discard(message);
}
}
}
}
return deleted;
}
public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
boolean changed = false;
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
synchronized (this.messages) {
for (Message message : this.messages) {
for (final DatabaseBackend.FilePathInfo file : files)
if (file.uuid.toString().equals(message.getUuid())) {
message.setDeleted(file.deleted);
changed = true;
if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
pgpDecryptionService.discard(message);
}
}
}
}
return changed;
}
public void clearMessages() {
synchronized (this.messages) {
this.messages.clear();
}
}
public boolean setIncomingChatState(ChatState state) {
if (this.mIncomingChatState == state) {
return false;
}
this.mIncomingChatState = state;
return true;
}
public ChatState getIncomingChatState() {
return this.mIncomingChatState;
}
public boolean setOutgoingChatState(ChatState state) {
if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
if (this.mOutgoingChatState != state) {
this.mOutgoingChatState = state;
return true;
}
}
return false;
}
public ChatState getOutgoingChatState() {
return this.mOutgoingChatState;
}
public void trim() {
synchronized (this.messages) {
final int size = messages.size();
final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
if (size > maxsize) {
List<Message> discards = this.messages.subList(0, size - maxsize);
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
if (pgpDecryptionService != null) {
pgpDecryptionService.discard(discards);
}
discards.clear();
untieMessages();
}
}
}
public void findUnsentTextMessages(OnMessageFound onMessageFound) {
final ArrayList<Message> results = new ArrayList<>();
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
results.add(message);
}
}
}
for (Message result : results) {
onMessageFound.onMessageFound(result);
}
}
public Message findSentMessageWithUuidOrRemoteId(String id) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (id.equals(message.getUuid())
|| (message.getStatus() >= Message.STATUS_SEND
&& id.equals(message.getRemoteMsgId()))) {
return message;
}
}
}
return null;
}
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
final Jid mcp = message.getCounterpart();
if (mcp == null && counterpart != null) {
continue;
}
if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
if (idMatch) return message;
}
}
}
return null;
}
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
final Jid mcp = message.getCounterpart();
if (mcp == null) {
continue;
}
if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
&& (carbon == message.isCarbon() || received)) {
final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
return message;
} else {
return null;
}
}
}
}
return null;
}
public Message findSentMessageWithUuid(String id) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (id.equals(message.getUuid())) {
return message;
}
}
}
return null;
}
public Message findMessageWithRemoteId(String id, Jid counterpart) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (counterpart.equals(message.getCounterpart())
&& (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
return message;
}
}
}
return null;
}
public Message findMessageWithServerMsgId(String id) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (id != null && id.equals(message.getServerMsgId())) {
return message;
}
}
}
return null;
}
public boolean hasMessageWithCounterpart(Jid counterpart) {
synchronized (this.messages) {
for (Message message : this.messages) {
if (counterpart.equals(message.getCounterpart())) {
return true;
}
}
}
return false;
}
public Message findMessageReactingTo(String id, Jid reactor) {
if (id == null) return null;
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
if (reactor == null && message.getStatus() < Message.STATUS_SEND) continue;
if (reactor != null && message.getCounterpart() == null) continue;
if (reactor != null && !(message.getCounterpart().equals(reactor) || message.getCounterpart().asBareJid().equals(reactor))) continue;
final Element r = message.getReactions();
if (r != null && r.getAttribute("id") != null && id.equals(r.getAttribute("id"))) {
return message;
}
}
}
return null;
}
public Set<String> findReactionsTo(String id, Jid reactor) {
Set<String> reactionEmoji = new HashSet<>();
Message reactM = findMessageReactingTo(id, reactor);
Element reactions = reactM == null ? null : reactM.getReactions();
if (reactions != null) {
for (Element el : reactions.getChildren()) {
if (el.getName().equals("reaction") && el.getNamespace().equals("urn:xmpp:reactions:0")) {
reactionEmoji.add(el.getContent());
}
}
}
return reactionEmoji;
}
public void populateWithMessages(final List<Message> messages) {
if (historyPartMessages.size() > 0) {
messages.clear();
messages.addAll(this.historyPartMessages);
} else {
synchronized (this.messages) {
messages.clear();
messages.addAll(this.messages);
}
}
}
@Override
public boolean isBlocked() {
return getContact().isBlocked();
}
@Override
public boolean isDomainBlocked() {
return getContact().isDomainBlocked();
}
@Override
public Jid getBlockedJid() {
return getContact().getBlockedJid();
}
public int countMessages() {
synchronized (this.messages) {
return this.messages.size();
}
}
public String getFirstMamReference() {
return this.mFirstMamReference;
}
public void setFirstMamReference(String reference) {
this.mFirstMamReference = reference;
}
public void setLastClearHistory(long time, String reference) {
if (reference != null) {
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
} else {
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
}
}
public MamReference getLastClearHistory() {
return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
}
public List<Jid> getAcceptedCryptoTargets() {
if (mode == MODE_SINGLE) {
return Collections.singletonList(getJid().asBareJid());
} else {
return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
}
}
public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
}
public boolean setCorrectingMessage(Message correctingMessage) {
setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
return correctingMessage == null && draftMessage != null;
}
public Message getCorrectingMessage() {
final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
return uuid == null ? null : findSentMessageWithUuid(uuid);
}
public boolean withSelf() {
return getContact().isSelf();
}
@Override
public int compareTo(@NonNull Conversation another) {
return ComparisonChain.start()
.compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false) && another.withSelf(), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false) && withSelf())
.compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
.compare(another.getSortableTime(), getSortableTime())
.result();
}
private long getSortableTime() {
Draft draft = getDraft();
long messageTime = getLatestMessage().getTimeSent();
if (draft == null) {
return messageTime;
} else {
return Math.max(messageTime, draft.getTimestamp());
}
}
public String getDraftMessage() {
return draftMessage;
}
public void setDraftMessage(String draftMessage) {
this.draftMessage = draftMessage;
}
public void setReplyTo(Message m) {
this.replyTo = m;
}
public Message getReplyTo() {
return this.replyTo;
}
public boolean isRead() {
synchronized (this.messages) {
for(final Message message : Lists.reverse(this.messages)) {
if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
continue;
}
return message.isRead();
}
return true;
}
}
public List<Message> markRead(String upToUuid) {
final List<Message> unread = new ArrayList<>();
synchronized (this.messages) {
for (Message message : this.messages) {
if (!message.isRead()) {
message.markRead();
unread.add(message);
}
if (message.getUuid().equals(upToUuid)) {
return unread;
}
}
}
return unread;
}
@Nullable
public Message getMessageWithAnyMatchingId(String uuid) {
if (uuid == null) {
return null;
}
synchronized (this.messages) {
for (int i = 0; i < messages.size(); ++i) {
if (uuid.equals(messages.get(i).getServerMsgId())) {
return messages.get(i);
}
if (uuid.equals(messages.get(i).getRemoteMsgId())) {
return messages.get(i);
}
if (uuid.equals(messages.get(i).getUuid())) {
return messages.get(i);
}
}
for (int i = 0; i < historyPartMessages.size(); ++i) {
if (uuid.equals(historyPartMessages.get(i).getServerMsgId())) {
return historyPartMessages.get(i);
}
if (uuid.equals(historyPartMessages.get(i).getRemoteMsgId())) {
return historyPartMessages.get(i);
}
if (uuid.equals(historyPartMessages.get(i).getUuid())) {
return historyPartMessages.get(i);
}
}
}
return null;
}
public Message getLatestMessage() {
synchronized (this.messages) {
if (this.messages.size() == 0) {
Message message = new Message(this, "", Message.ENCRYPTION_NONE);
message.setType(Message.TYPE_STATUS);
message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
return message;
} else {
return this.messages.get(this.messages.size() - 1);
}
}
}
public @NonNull
CharSequence getName() {
if (getMode() == MODE_MULTI) {
final String roomName = getMucOptions().getName();
final String subject = getMucOptions().getSubject();
final Bookmark bookmark = getBookmark();
final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
if (printableValue(roomName)) {
return roomName;
} else if (printableValue(subject)) {
return subject;
} else if (printableValue(bookmarkName, false)) {
return bookmarkName;
} else {
final String generatedName = getMucOptions().createNameFromParticipants();
if (printableValue(generatedName)) {
return generatedName;
} else {
return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
}
}
} else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
return contactJid;
} else {
return this.getContact().getDisplayName();
}
}
public String getAccountUuid() {
return this.accountUuid;
}
public Jid getContactJid() {
return contactJid;
}
public Account getAccount() {
return this.account;
}
public void setAccount(final Account account) {
this.account = account;
}
public Contact getContact() {
return this.account.getRoster().getContact(this.contactJid);
}
@Override
public Jid getJid() {
return this.contactJid;
}
public int getStatus() {
return this.status;
}
public void setStatus(int status) {
this.status = status;
}
public long getCreated() {
return this.created;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put(UUID, uuid);
values.put(NAME, name);
values.put(CONTACT, contactUuid);
values.put(ACCOUNT, accountUuid);
values.put(CONTACTJID, contactJid.toString());
values.put(CREATED, created);
values.put(STATUS, status);
values.put(MODE, mode);
if (nextCounterpart != null) {
values.put(NEXT_COUNTERPART, nextCounterpart.toString());
}
synchronized (this.attributes) {
values.put(ATTRIBUTES, attributes.toString());
}
return values;
}
public int getMode() {
return this.mode;
}
public void setMode(int mode) {
this.mode = mode;
}
/**
* short for is Private and Non-anonymous
*/
public boolean isSingleOrPrivateAndNonAnonymous() {
return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
}
public boolean isPrivateAndNonAnonymous() {
return getMucOptions().isPrivateAndNonAnonymous();
}
public synchronized MucOptions getMucOptions() {
Conversation parent = parentConversation == null ? null : parentConversation.get();
if (parent != null) {
this.mucOptions = parent.getMucOptions();
}
if (this.mucOptions == null) {
this.mucOptions = new MucOptions(this);
}
return this.mucOptions;
}
public void resetMucOptions() {
this.mucOptions = null;
}
public void setContactJid(final Jid jid) {
this.contactJid = jid;
}
public Jid getNextCounterpart() {
return this.nextCounterpart;
}
public void setNextCounterpart(Jid jid) {
this.nextCounterpart = jid;
}
public int getNextEncryption() {
if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
return Message.ENCRYPTION_NONE;
}
if (OmemoSetting.isAlways()) {
return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
}
final int defaultEncryption;
if (suitableForOmemoByDefault(this)) {
defaultEncryption = OmemoSetting.getEncryption();
} else {
defaultEncryption = Message.ENCRYPTION_NONE;
}
int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
return defaultEncryption;
} else {
return encryption;
}
}
public boolean setNextEncryption(int encryption) {
return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
}
public String getNextMessage() {
final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
return nextMessage == null ? "" : nextMessage;
}
public @Nullable
Draft getDraft() {
long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
if (timestamp > getLatestMessage().getTimeSent()) {
String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
if (!TextUtils.isEmpty(message) && timestamp != 0) {
return new Draft(message, timestamp);
}
}
return null;
}
public boolean setNextMessage(final String input) {
final String message = input == null || input.trim().isEmpty() ? null : input;
boolean changed = !getNextMessage().equals(message);
this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
if (changed) {
this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
}
return changed;
}
public Bookmark getBookmark() {
return this.account.getBookmark(this.contactJid);
}
public Message findDuplicateMessage(Message message) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
if (this.messages.get(i).similar(message)) {
return this.messages.get(i);
}
}
}
return null;
}
public boolean hasDuplicateMessage(Message message) {
return findDuplicateMessage(message) != null;
}
public Message findSentMessageWithBody(String body) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
Message message = this.messages.get(i);
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
String otherBody;
if (message.hasFileOnRemoteHost()) {
otherBody = message.getFileParams().url;
} else {
otherBody = message.body;
}
if (otherBody != null && otherBody.equals(body)) {
return message;
}
}
}
return null;
}
}
public Message findRtpSession(final String sessionId, final int s) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = this.messages.get(i);
if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
return message;
}
}
}
return null;
}
public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
if (serverMsgId == null || remoteMsgId == null) {
return false;
}
synchronized (this.messages) {
for (Message message : this.messages) {
if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
return true;
}
}
}
return false;
}
public MamReference getLastMessageTransmitted() {
final MamReference lastClear = getLastClearHistory();
MamReference lastReceived = new MamReference(0);
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = this.messages.get(i);
if (message.isPrivateMessage()) {
continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
}
if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
break;
}
}
}
return MamReference.max(lastClear, lastReceived);
}
public void setMutedTill(long value) {
this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
}
public boolean isMuted() {
return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
}
public boolean alwaysNotify() {
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
}
public boolean setAttribute(String key, boolean value) {
return setAttribute(key, String.valueOf(value));
}
private boolean setAttribute(String key, long value) {
return setAttribute(key, Long.toString(value));
}
private boolean setAttribute(String key, int value) {
return setAttribute(key, String.valueOf(value));
}
public boolean setAttribute(String key, String value) {
synchronized (this.attributes) {
try {
if (value == null) {
if (this.attributes.has(key)) {
this.attributes.remove(key);
return true;
} else {
return false;
}
} else {
final String prev = this.attributes.optString(key, null);
this.attributes.put(key, value);
return !value.equals(prev);
}
} catch (JSONException e) {
throw new AssertionError(e);
}
}
}
public boolean setAttribute(String key, List<Jid> jids) {
JSONArray array = new JSONArray();
for (Jid jid : jids) {
array.put(jid.asBareJid().toString());
}
synchronized (this.attributes) {
try {
this.attributes.put(key, array);
return true;
} catch (JSONException e) {
return false;
}
}
}
public String getAttribute(String key) {
synchronized (this.attributes) {
return this.attributes.optString(key, null);
}
}
private List<Jid> getJidListAttribute(String key) {
ArrayList<Jid> list = new ArrayList<>();
synchronized (this.attributes) {
try {
JSONArray array = this.attributes.getJSONArray(key);
for (int i = 0; i < array.length(); ++i) {
try {
list.add(Jid.of(array.getString(i)));
} catch (IllegalArgumentException e) {
//ignored
}
}
} catch (JSONException e) {
//ignored
}
}
return list;
}
private int getIntAttribute(String key, int defaultValue) {
String value = this.getAttribute(key);
if (value == null) {
return defaultValue;
} else {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
public long getLongAttribute(String key, long defaultValue) {
String value = this.getAttribute(key);
if (value == null) {
return defaultValue;
} else {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
public boolean getBooleanAttribute(String key, boolean defaultValue) {
String value = this.getAttribute(key);
if (value == null) {
return defaultValue;
} else {
return Boolean.parseBoolean(value);
}
}
public void add(Message message) {
String res1 = message.getCounterpart() == null ? null : message.getCounterpart().getResource();
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
if (nextCounterpart == null) {
if (!message.isPrivateMessage()) {
synchronized (this.messages) {
this.messages.add(message);
actualizeReplyMessages(this.messages, List.of(message));
}
}
} else {
if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
synchronized (this.messages) {
this.messages.add(message);
actualizeReplyMessages(this.messages, List.of(message));
}
}
}
}
public void prepend(int offset, Message message) {
String res1 = message.getCounterpart() == null ? null : message.getCounterpart().getResource();
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
List<Message> properListToAdd;
if (!historyPartMessages.isEmpty()) {
properListToAdd = historyPartMessages;
} else {
properListToAdd = this.messages;
}
if (nextCounterpart == null) {
if (!message.isPrivateMessage()) {
synchronized (this.messages) {
properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
actualizeReplyMessages(properListToAdd, List.of(message));
}
}
} else {
if (message.isPrivateMessage() && Objects.equals(res1, res2)) {
synchronized (this.messages) {
properListToAdd.add(Math.min(offset, properListToAdd.size()), message);
actualizeReplyMessages(properListToAdd, List.of(message));
}
}
}
synchronized (this.messages) {
if (!historyPartMessages.isEmpty() && hasDuplicateMessage(historyPartMessages.get(historyPartMessages.size() - 1))) {
messages.addAll(0, historyPartMessages);
actualizeReplyMessages(messages, List.of(message));
jumpToLatest();
}
}
}
public void addAll(int index, List<Message> messages, boolean fromPagination) {
if (messages.isEmpty()) return;
List<Message> newM = new ArrayList<>();
if (nextCounterpart == null) {
for(Message m : messages) {
if (!m.isPrivateMessage()) {
newM.add(m);
}
}
} else {
for(Message m : messages) {
String res1 = m.getCounterpart() == null ? null : m.getCounterpart().getResource();
String res2 = nextCounterpart == null ? null : nextCounterpart.getResource();
if (m.isPrivateMessage() && Objects.equals(res1, res2)) {
newM.add(m);
}
}
}
synchronized (this.messages) {
List<Message> properListToAdd;
if (fromPagination && !historyPartMessages.isEmpty() && checkIsMergeable(newM)) {
historyPartMessages.addAll(newM);
newM = filterExisted(historyPartMessages);
index = 0;
jumpToLatest();
}
if (fromPagination && !historyPartMessages.isEmpty()) {
properListToAdd = historyPartMessages;
} else {
properListToAdd = this.messages;
}
if (index == -1) {
properListToAdd.addAll(newM);
} else {
properListToAdd.addAll(index, newM);
}
actualizeReplyMessages(properListToAdd, messages);
}
account.getPgpDecryptionService().decrypt(newM);
}
private void actualizeReplyMessages(List<Message> mainList, List<Message> messages) {
for (Message m : mainList) {
if (m.isReplyRestoredFromDb()) {
Element reply = m.getReply();
if (reply == null) {
continue;
}
String replyId = reply.getAttribute("id");
for (Message rep : messages) {
if (replyId.equals(rep.getServerMsgId())) {
m.setReplyMessage(rep, false);
break;
}
if (replyId.equals(rep.getRemoteMsgId())) {
m.setReplyMessage(rep, false);
break;
}
if (replyId.equals(rep.getUuid())) {
m.setReplyMessage(rep, false);
break;
}
}
}
}
}
public void expireOldMessages(long timestamp) {
synchronized (this.messages) {
for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
if (iterator.next().getTimeSent() < timestamp) {
iterator.remove();
}
}
untieMessages();
}
}
public void sort() {
synchronized (this.messages) {
Collections.sort(this.messages, (left, right) -> {
if (left.getTimeSent() < right.getTimeSent()) {
return -1;
} else if (left.getTimeSent() > right.getTimeSent()) {
return 1;
} else {
return 0;
}
});
untieMessages();
}
}
public void jumpToHistoryPart(List<Message> messages) {
historyPartMessages.clear();
if (checkIsMergeable(messages)) {
addAll(0, filterExisted(messages), false);
} else {
historyPartMessages.addAll(messages);
}
}
public void jumpToLatest() {
historyPartMessages.clear();
}
public boolean isInHistoryPart() {
return !historyPartMessages.isEmpty();
}
private boolean checkIsMergeable(List<Message> messages) {
if (messages.isEmpty()) return true;
return findDuplicateMessage(messages.get(messages.size() - 1)) != null;
}
private List<Message> filterExisted(List<Message> messages) {
if (messages.isEmpty()) return Collections.emptyList();
List<Message> result = new ArrayList<>();
for (Message m : messages) {
if (findDuplicateMessage(m) == null) {
result.add(m);
}
}
return result;
}
private void untieMessages() {
for (Message message : this.messages) {
message.untie();
}
}
public int unreadCount() {
synchronized (this.messages) {
int count = 0;
for(final Message message : Lists.reverse(this.messages)) {
if (message.isRead()) {
if (message.getType() == Message.TYPE_RTP_SESSION) {
continue;
}
return count;
}
++count;
}
return count;
}
}
public int receivedMessagesCount() {
int count = 0;
synchronized (this.messages) {
for (Message message : messages) {
if (message.getStatus() == Message.STATUS_RECEIVED) {
++count;
}
}
}
return count;
}
public int sentMessagesCount() {
int count = 0;
synchronized (this.messages) {
for (Message message : messages) {
if (message.getStatus() != Message.STATUS_RECEIVED) {
++count;
}
}
}
return count;
}
public boolean isWithStranger() {
final Contact contact = getContact();
return mode == MODE_SINGLE
&& !contact.isOwnServer()
&& !contact.showInContactList()
&& !contact.isSelf()
&& !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
&& sentMessagesCount() == 0;
}
public int getReceivedMessagesCountSinceUuid(String uuid) {
if (uuid == null) {
return 0;
}
int count = 0;
synchronized (this.messages) {
for (int i = messages.size() - 1; i >= 0; i--) {
final Message message = messages.get(i);
if (uuid.equals(message.getUuid())) {
return count;
}
if (message.getStatus() <= Message.STATUS_RECEIVED) {
++count;
}
}
}
return 0;
}
@Override
public int getAvatarBackgroundColor() {
return UIHelper.getColorForName(getName().toString());
}
@Override
public String getAvatarName() {
return getName().toString();
}
public void setCurrentTab(int tab) {
mCurrentTab = tab;
}
public int getCurrentTab() {
if (mCurrentTab >= 0) return mCurrentTab;
if (!isRead() || getContact().resourceWhichSupport(Namespace.COMMANDS) == null) {
return 0;
}
return 1;
}
public void refreshSessions() {
pagerAdapter.refreshSessions();
}
public void startCommand(Element command, XmppConnectionService xmppConnectionService, Context activityContext) {
pagerAdapter.startCommand(command, xmppConnectionService, activityContext);
}
public void setupViewPager(ViewPager pager, TabLayout tabs, Conversation oldConversation) {
pagerAdapter.setupViewPager(pager, tabs, oldConversation);
}
public void showViewPager() {
pagerAdapter.show();
}
public void hideViewPager() {
pagerAdapter.hide();
}
public interface OnMessageFound {
void onMessageFound(final Message message);
}
public static class Draft {
private final String message;
private final long timestamp;
private Draft(String message, long timestamp) {
this.message = message;
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
}
public interface ConversationPage {
public String getTitle();
public String getNode();
public View inflateUi(Context context, Consumer<ConversationPage> remover);
public View getView();
public void refresh();
}
public class ConversationPagerAdapter extends PagerAdapter {
protected ViewPager mPager = null;
protected TabLayout mTabs = null;
ArrayList<ConversationPage> sessions = null;
protected View page1 = null;
protected View page2 = null;
public void setupViewPager(ViewPager pager, TabLayout tabs, Conversation oldConversation) {
mPager = pager;
mTabs = tabs;
if (oldConversation != null) {
oldConversation.pagerAdapter.mPager = null;
oldConversation.pagerAdapter.mTabs = null;
}
if (mPager == null) {
page1 = null;
page2 = null;
return;
}
if (sessions != null) show();
if (pager.getChildAt(0) != null) page1 = pager.getChildAt(0);
if (pager.getChildAt(1) != null) page2 = pager.getChildAt(1);
if (page2 != null && page2.findViewById(R.id.commands_view) == null) {
page1 = null;
page2 = null;
}
if (page1 == null) page1 = oldConversation.pagerAdapter.page1;
if (page2 == null) page2 = oldConversation.pagerAdapter.page2;
if (page1 == null || page2 == null) {
throw new IllegalStateException("page1 or page2 were not present as child or in model?");
}
pager.removeView(page1);
pager.removeView(page2);
pager.setAdapter(this);
tabs.setupWithViewPager(mPager);
pager.post(() -> pager.setCurrentItem(getCurrentTab()));
mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
public void onPageScrollStateChanged(int state) { }
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { }
public void onPageSelected(int position) {
setCurrentTab(position);
}
});
}
public void show() {
if (sessions == null) {
sessions = new ArrayList<>();
notifyDataSetChanged();
}
if (mTabs != null) mTabs.setVisibility(View.VISIBLE);
}
public void hide() {
if (sessions != null && !sessions.isEmpty()) return; // Do not hide during active session
if (mPager != null) mPager.setCurrentItem(0);
if (mTabs != null) mTabs.setVisibility(View.GONE);
sessions = null;
notifyDataSetChanged();
}
public void refreshSessions() {
if (sessions == null) return;
for (ConversationPage session : sessions) {
session.refresh();
}
}
public void startCommand(Element command, XmppConnectionService xmppConnectionService, Context activityContext) {
show();
CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService, activityContext);
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(command.getAttributeAsJid("jid"));
final Element c = packet.addChild("command", Namespace.COMMANDS);
c.setAttribute("node", command.getAttribute("node"));
c.setAttribute("action", "execute");
final TimerTask task = new TimerTask() {
@Override
public void run() {
if (getAccount().getStatus() != Account.State.ONLINE) {
final TimerTask self = this;
new Timer().schedule(new TimerTask() {
@Override
public void run() {
self.run();
}
}, 1000);
} else {
xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
session.updateWithResponse(iq);
}, 120L);
}
}
};
task.run();
sessions.add(session);
notifyDataSetChanged();
if (mPager != null) mPager.setCurrentItem(getCount() - 1);
}
public void removeSession(ConversationPage session) {
sessions.remove(session);
notifyDataSetChanged();
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
if (position == 0) {
if (page1 != null && page1.getParent() != null) {
((ViewGroup) page1.getParent()).removeView(page1);
}
container.addView(page1);
return page1;
}
if (position == 1) {
if (page2 != null && page2.getParent() != null) {
((ViewGroup) page2.getParent()).removeView(page2);
}
container.addView(page2);
return page2;
}
ConversationPage session = sessions.get(position-2);
View v = session.inflateUi(container.getContext(), (s) -> removeSession(s));
if (v != null && v.getParent() != null) {
((ViewGroup) v.getParent()).removeView(v);
}
container.addView(v);
return session;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, Object o) {
if (position < 2) {
container.removeView((View) o);
return;
}
container.removeView(((ConversationPage) o).getView());
}
@Override
public int getItemPosition(Object o) {
if (mPager != null) {
if (o == page1) return PagerAdapter.POSITION_UNCHANGED;
if (o == page2) return PagerAdapter.POSITION_UNCHANGED;
}
int pos = sessions == null ? -1 : sessions.indexOf(o);
if (pos < 0) return PagerAdapter.POSITION_NONE;
return pos + 2;
}
@Override
public int getCount() {
if (sessions == null) return 1;
int count = 2 + sessions.size();
if (mTabs == null) return count;
if (count > 2) {
mTabs.setTabMode(TabLayout.MODE_SCROLLABLE);
} else {
mTabs.setTabMode(TabLayout.MODE_FIXED);
}
return count;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
if (view == o) return true;
if (o instanceof ConversationPage) {
return ((ConversationPage) o).getView() == view;
}
return false;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return "Conversation";
case 1:
return "Commands";
default:
ConversationPage session = sessions.get(position-2);
if (session == null) return super.getPageTitle(position);
return session.getTitle();
}
}
class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> implements ConversationPage {
abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected T binding;
public ViewHolder(T binding) {
super(binding.getRoot());
this.binding = binding;
}
abstract public void bind(Item el);
protected void setTextOrHide(TextView v, Optional<String> s) {
if (s == null || !s.isPresent()) {
v.setVisibility(View.GONE);
} else {
v.setVisibility(View.VISIBLE);
v.setText(s.get());
}
}
protected void setupInputType(Element field, TextView textinput, TextInputLayout layout) {
int flags = 0;
if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_NONE);
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
String type = field.getAttribute("type");
if (type != null) {
if (type.equals("text-multi") || type.equals("jid-multi")) {
flags |= InputType.TYPE_TEXT_FLAG_MULTI_LINE;
}
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
if (type.equals("jid-single") || type.equals("jid-multi")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
}
if (type.equals("text-private")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
if (layout != null) layout.setEndIconMode(TextInputLayout.END_ICON_PASSWORD_TOGGLE);
}
}
Element validate = field.findChild("validate", "http://jabber.org/protocol/xdata-validate");
if (validate == null) return;
String datatype = validate.getAttribute("datatype");
if (datatype == null) return;
if (datatype.equals("xs:integer") || datatype.equals("xs:int") || datatype.equals("xs:long") || datatype.equals("xs:short") || datatype.equals("xs:byte")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
if (datatype.equals("xs:decimal") || datatype.equals("xs:double")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED | InputType.TYPE_NUMBER_FLAG_DECIMAL);
}
if (datatype.equals("xs:date")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE);
}
if (datatype.equals("xs:dateTime")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL);
}
if (datatype.equals("xs:time")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME);
}
if (datatype.equals("xs:anyURI")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
}
if (datatype.equals("html:tel")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_PHONE);
}
if (datatype.equals("html:email")) {
textinput.setInputType(flags | InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
}
}
protected String formatValue(String datatype, String value, boolean compact) {
if ("xs:dateTime".equals(datatype)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ZonedDateTime zonedDateTime = null;
try {
zonedDateTime = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
} catch (final DateTimeParseException e) {
try {
DateTimeFormatter almostIso = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm[:ss] X");
zonedDateTime = ZonedDateTime.parse(value, almostIso);
} catch (final DateTimeParseException e2) {
}
}
if (zonedDateTime == null) return value;
ZonedDateTime localZonedDateTime = zonedDateTime.withZoneSameInstant(ZoneId.systemDefault());
DateTimeFormatter outputFormat = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
return localZonedDateTime.toLocalDateTime().format(outputFormat);
} else {
return value;
}
}
if ("html:tel".equals(datatype) && !compact) {
return PhoneNumberUtils.formatNumber(value, value, null);
}
return value;
}
}
class ErrorViewHolder extends ViewHolder<CommandNoteBinding> {
public ErrorViewHolder(CommandNoteBinding binding) { super(binding); }
@Override
public void bind(Item iq) {
binding.errorIcon.setVisibility(View.VISIBLE);
Element error = iq.el.findChild("error");
if (error == null) return;
String text = error.findChildContent("text", "urn:ietf:params:xml:ns:xmpp-stanzas");
if (text == null || text.equals("")) {
text = error.getChildren().get(0).getName();
}
binding.message.setText(text);
}
}
class NoteViewHolder extends ViewHolder<CommandNoteBinding> {
public NoteViewHolder(CommandNoteBinding binding) { super(binding); }
@Override
public void bind(Item note) {
binding.message.setText(note.el.getContent());
String type = note.el.getAttribute("type");
if (type != null && type.equals("error")) {
binding.errorIcon.setVisibility(View.VISIBLE);
}
}
}
class ResultFieldViewHolder extends ViewHolder<CommandResultFieldBinding> {
public ResultFieldViewHolder(CommandResultFieldBinding binding) { super(binding); }
@Override
public void bind(Item item) {
Field field = (Field) item;
setTextOrHide(binding.label, field.getLabel());
setTextOrHide(binding.desc, field.getDesc());
Element media = field.el.findChild("media", "urn:xmpp:media-element");
if (media == null) {
binding.mediaImage.setVisibility(View.GONE);
} else {
for (Element uriEl : media.getChildren()) {
if (!"uri".equals(uriEl.getName())) continue;
if (!"urn:xmpp:media-element".equals(uriEl.getNamespace())) continue;
String mimeType = uriEl.getAttribute("type");
String uriS = uriEl.getContent();
if (mimeType == null || uriS == null) continue;
Uri uri = Uri.parse(uriS);
if (mimeType.startsWith("image/") && "https".equals(uri.getScheme())) {
binding.mediaImage.setVisibility(View.VISIBLE);
Glide.with(binding.getRoot().getContext())
.load(uri)
.centerCrop()
.into(binding.mediaImage);
}
}
}
Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
String datatype = validate == null ? null : validate.getAttribute("datatype");
ArrayAdapter<Option> values = new ArrayAdapter<>(binding.getRoot().getContext(), R.layout.simple_list_item);
for (Element el : field.el.getChildren()) {
if (el.getName().equals("value") && el.getNamespace().equals("jabber:x:data")) {
values.add(new Option(el.getContent(), formatValue(datatype, el.getContent(), false)));
}
}
binding.values.setAdapter(values);
justifyListViewHeightBasedOnChildren(binding.values);
if (field.getType().equals(Optional.of("jid-single")) || field.getType().equals(Optional.of("jid-multi"))) {
binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
new FixedURLSpan("xmpp:" + Jid.ofEscaped(values.getItem(pos).getValue()).toEscapedString(), account).onClick(binding.values);
});
} else if ("xs:anyURI".equals(datatype)) {
binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
new FixedURLSpan(values.getItem(pos).getValue(), account).onClick(binding.values);
});
} else if ("html:tel".equals(datatype)) {
binding.values.setOnItemClickListener((arg0, arg1, pos, id) -> {
try {
new FixedURLSpan("tel:" + PhoneNumberUtilWrapper.normalize(binding.getRoot().getContext(), values.getItem(pos).getValue()), account).onClick(binding.values);
} catch (final IllegalArgumentException | NumberParseException | NullPointerException e) { }
});
}
binding.values.setOnItemLongClickListener((arg0, arg1, pos, id) -> {
if (ShareUtil.copyTextToClipboard(binding.getRoot().getContext(), values.getItem(pos).getValue(), R.string.message)) {
Toast.makeText(binding.getRoot().getContext(), R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
return true;
});
}
private void justifyListViewHeightBasedOnChildren (ListView listView) {
ListAdapter adapter = listView.getAdapter();
if (adapter == null) {
return;
}
ViewGroup vg = listView;
int totalHeight = 0;
for (int i = 0; i < adapter.getCount(); i++) {
View listItem = adapter.getView(i, null, vg);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams par = listView.getLayoutParams();
par.height = totalHeight + (listView.getDividerHeight() * (adapter.getCount() - 1));
listView.setLayoutParams(par);
listView.requestLayout();
}
}
class ResultCellViewHolder extends ViewHolder<CommandResultCellBinding> {
public ResultCellViewHolder(CommandResultCellBinding binding) { super(binding); }
@Override
public void bind(Item item) {
Cell cell = (Cell) item;
if (cell.el == null) {
binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Subhead);
setTextOrHide(binding.text, cell.reported.getLabel());
} else {
Element validate = cell.reported.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
String datatype = validate == null ? null : validate.getAttribute("datatype");
String value = formatValue(datatype, cell.el.findChildContent("value", "jabber:x:data"), true);
SpannableStringBuilder text = new SpannableStringBuilder(value == null ? "" : value);
if (cell.reported.getType().equals(Optional.of("jid-single"))) {
text.setSpan(new FixedURLSpan("xmpp:" + Jid.ofEscaped(text.toString()).toEscapedString(), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if ("xs:anyURI".equals(datatype)) {
text.setSpan(new FixedURLSpan(text.toString(), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if ("html:tel".equals(datatype)) {
try {
text.setSpan(new FixedURLSpan("tel:" + PhoneNumberUtilWrapper.normalize(binding.getRoot().getContext(), text.toString()), account), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} catch (final IllegalArgumentException | NumberParseException | NullPointerException e) { }
}
binding.text.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Body1);
binding.text.setText(text);
BetterLinkMovementMethod method = BetterLinkMovementMethod.newInstance();
method.setOnLinkLongClickListener((tv, url) -> {
tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
ShareUtil.copyLinkToClipboard(binding.getRoot().getContext(), url);
return true;
});
binding.text.setMovementMethod(method);
Linkify.addLinks(binding.text, Linkify.ALL);
}
}
}
class ItemCardViewHolder extends ViewHolder<CommandItemCardBinding> {
public ItemCardViewHolder(CommandItemCardBinding binding) { super(binding); }
@Override
public void bind(Item item) {
binding.fields.removeAllViews();
for (Field field : reported) {
CommandResultFieldBinding row = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.command_result_field, binding.fields, false);
GridLayout.LayoutParams param = new GridLayout.LayoutParams();
param.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f);
param.width = 0;
row.getRoot().setLayoutParams(param);
binding.fields.addView(row.getRoot());
for (Element el : item.el.getChildren()) {
if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data") && el.getAttribute("var") != null && el.getAttribute("var").equals(field.getVar())) {
for (String label : field.getLabel().asSet()) {
el.setAttribute("label", label);
}
for (String desc : field.getDesc().asSet()) {
el.setAttribute("desc", desc);
}
for (String type : field.getType().asSet()) {
el.setAttribute("type", type);
}
Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
if (validate != null) el.addChild(validate);
new ResultFieldViewHolder(row).bind(new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), -1));
}
}
}
}
}
class CheckboxFieldViewHolder extends ViewHolder<CommandCheckboxFieldBinding> implements CompoundButton.OnCheckedChangeListener {
public CheckboxFieldViewHolder(CommandCheckboxFieldBinding binding) {
super(binding);
binding.row.setOnClickListener((v) -> {
binding.checkbox.toggle();
});
binding.checkbox.setOnCheckedChangeListener(this);
}
protected Element mValue = null;
@Override
public void bind(Item item) {
Field field = (Field) item;
binding.label.setText(field.getLabel().or(""));
setTextOrHide(binding.desc, field.getDesc());
mValue = field.getValue();
binding.checkbox.setChecked(mValue.getContent() != null && (mValue.getContent().equals("true") || mValue.getContent().equals("1")));
}
@Override
public void onCheckedChanged(CompoundButton checkbox, boolean isChecked) {
if (mValue == null) return;
mValue.setContent(isChecked ? "true" : "false");
}
}
class SearchListFieldViewHolder extends ViewHolder<CommandSearchListFieldBinding> implements TextWatcher {
public SearchListFieldViewHolder(CommandSearchListFieldBinding binding) {
super(binding);
binding.search.addTextChangedListener(this);
}
protected Element mValue = null;
List<Option> options = new ArrayList<>();
protected ArrayAdapter<Option> adapter;
protected boolean open;
@Override
public void bind(Item item) {
Field field = (Field) item;
setTextOrHide(binding.label, field.getLabel());
setTextOrHide(binding.desc, field.getDesc());
if (field.error != null) {
binding.desc.setVisibility(View.VISIBLE);
binding.desc.setText(field.error);
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
} else {
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
}
mValue = field.getValue();
Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
open = validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null;
setupInputType(field.el, binding.search, null);
options = field.getOptions();
binding.list.setOnItemClickListener((parent, view, position, id) -> {
mValue.setContent(adapter.getItem(binding.list.getCheckedItemPosition()).getValue());
if (open) binding.search.setText(mValue.getContent());
});
search("");
}
@Override
public void afterTextChanged(Editable s) {
if (open) mValue.setContent(s.toString());
search(s.toString());
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int count, int after) { }
protected void search(String s) {
List<Option> filteredOptions;
final String q = s.replaceAll("\\W", "").toLowerCase();
if (q == null || q.equals("")) {
filteredOptions = options;
} else {
filteredOptions = options.stream().filter(o -> o.toString().replaceAll("\\W", "").toLowerCase().contains(q)).collect(Collectors.toList());
}
adapter = new ArrayAdapter(binding.getRoot().getContext(), R.layout.simple_list_item_selectable_text, filteredOptions);
binding.list.setAdapter(adapter);
int checkedPos = filteredOptions.indexOf(new Option(mValue.getContent(), ""));
if (checkedPos >= 0) binding.list.setItemChecked(checkedPos, true);
}
}
class RadioEditFieldViewHolder extends ViewHolder<CommandRadioEditFieldBinding> implements CompoundButton.OnCheckedChangeListener, TextWatcher {
public RadioEditFieldViewHolder(CommandRadioEditFieldBinding binding) {
super(binding);
binding.open.addTextChangedListener(this);
options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.radio_grid_item) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
CompoundButton v = (CompoundButton) super.getView(position, convertView, parent);
v.setId(position);
v.setChecked(getItem(position).getValue().equals(mValue.getContent()));
v.setOnCheckedChangeListener(RadioEditFieldViewHolder.this);
return v;
}
};
}
protected Element mValue = null;
protected ArrayAdapter<Option> options;
@Override
public void bind(Item item) {
Field field = (Field) item;
setTextOrHide(binding.label, field.getLabel());
setTextOrHide(binding.desc, field.getDesc());
if (field.error != null) {
binding.desc.setVisibility(View.VISIBLE);
binding.desc.setText(field.error);
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
} else {
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
}
mValue = field.getValue();
Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
binding.open.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
binding.open.setText(mValue.getContent());
setupInputType(field.el, binding.open, null);
options.clear();
List<Option> theOptions = field.getOptions();
options.addAll(theOptions);
float screenWidth = binding.getRoot().getContext().getResources().getDisplayMetrics().widthPixels;
TextPaint paint = ((TextView) LayoutInflater.from(binding.getRoot().getContext()).inflate(R.layout.radio_grid_item, null)).getPaint();
float maxColumnWidth = theOptions.stream().map((x) ->
StaticLayout.getDesiredWidth(x.toString(), paint)
).max(Float::compare).orElse(new Float(0.0));
if (maxColumnWidth * theOptions.size() < 0.90 * screenWidth) {
binding.radios.setNumColumns(theOptions.size());
} else if (maxColumnWidth * (theOptions.size() / 2) < 0.90 * screenWidth) {
binding.radios.setNumColumns(theOptions.size() / 2);
} else {
binding.radios.setNumColumns(1);
}
binding.radios.setAdapter(options);
}
@Override
public void onCheckedChanged(CompoundButton radio, boolean isChecked) {
if (mValue == null) return;
if (isChecked) {
mValue.setContent(options.getItem(radio.getId()).getValue());
binding.open.setText(mValue.getContent());
}
options.notifyDataSetChanged();
}
@Override
public void afterTextChanged(Editable s) {
if (mValue == null) return;
mValue.setContent(s.toString());
options.notifyDataSetChanged();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int count, int after) { }
}
class SpinnerFieldViewHolder extends ViewHolder<CommandSpinnerFieldBinding> implements AdapterView.OnItemSelectedListener {
public SpinnerFieldViewHolder(CommandSpinnerFieldBinding binding) {
super(binding);
binding.spinner.setOnItemSelectedListener(this);
}
protected Element mValue = null;
@Override
public void bind(Item item) {
Field field = (Field) item;
setTextOrHide(binding.label, field.getLabel());
binding.spinner.setPrompt(field.getLabel().or(""));
setTextOrHide(binding.desc, field.getDesc());
mValue = field.getValue();
ArrayAdapter<Option> options = new ArrayAdapter<Option>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item);
options.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
options.addAll(field.getOptions());
binding.spinner.setAdapter(options);
binding.spinner.setSelection(options.getPosition(new Option(mValue.getContent(), null)));
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
Option o = (Option) parent.getItemAtPosition(pos);
if (mValue == null) return;
mValue.setContent(o == null ? "" : o.getValue());
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
mValue.setContent("");
}
}
class ButtonGridFieldViewHolder extends ViewHolder<CommandButtonGridFieldBinding> {
public ButtonGridFieldViewHolder(CommandButtonGridFieldBinding binding) {
super(binding);
options = new ArrayAdapter<Option>(binding.getRoot().getContext(), R.layout.button_grid_item) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Button v = (Button) super.getView(position, convertView, parent);
v.setOnClickListener((view) -> {
mValue.setContent(getItem(position).getValue());
execute();
loading = true;
});
final SVG icon = getItem(position).getIcon();
if (icon != null) {
v.post(() -> {
if (v.getHeight() == 0) return;
icon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
Bitmap bitmap = Bitmap.createBitmap(v.getHeight(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas bmcanvas = new Canvas(bitmap);
icon.renderToCanvas(bmcanvas);
v.setCompoundDrawablesRelativeWithIntrinsicBounds(new BitmapDrawable(bitmap), null, null, null);
});
}
return v;
}
};
}
protected Element mValue = null;
protected ArrayAdapter<Option> options;
protected Option defaultOption = null;
@Override
public void bind(Item item) {
Field field = (Field) item;
setTextOrHide(binding.label, field.getLabel());
setTextOrHide(binding.desc, field.getDesc());
if (field.error != null) {
binding.desc.setVisibility(View.VISIBLE);
binding.desc.setText(field.error);
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Design_Error);
} else {
binding.desc.setTextAppearance(binding.getRoot().getContext(), R.style.TextAppearance_Conversations_Status);
}
mValue = field.getValue();
Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
binding.openButton.setVisibility((validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null) ? View.VISIBLE : View.GONE);
binding.openButton.setOnClickListener((view) -> {
AlertDialog.Builder builder = new AlertDialog.Builder(binding.getRoot().getContext());
DialogQuickeditBinding dialogBinding = DataBindingUtil.inflate(LayoutInflater.from(binding.getRoot().getContext()), R.layout.dialog_quickedit, null, false);
builder.setPositiveButton(R.string.action_execute, null);
if (field.getDesc().isPresent()) {
dialogBinding.inputLayout.setHint(field.getDesc().get());
}
dialogBinding.inputEditText.requestFocus();
dialogBinding.inputEditText.getText().append(mValue.getContent());
builder.setView(dialogBinding.getRoot());
builder.setNegativeButton(R.string.cancel, null);
final AlertDialog dialog = builder.create();
dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(dialogBinding.inputEditText));
dialog.show();
View.OnClickListener clickListener = v -> {
String value = dialogBinding.inputEditText.getText().toString();
mValue.setContent(value);
SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
dialog.dismiss();
execute();
loading = true;
};
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
dialog.dismiss();
}));
dialog.setCanceledOnTouchOutside(false);
dialog.setOnDismissListener(dialog1 -> {
SoftKeyboardUtils.hideSoftKeyboard(dialogBinding.inputEditText);
});
});
options.clear();
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();
defaultOption = null;
for (Option option : theOptions) {
if (option.getValue().equals(mValue.getContent())) {
defaultOption = option;
break;
}
}
if (defaultOption == null && mValue.getContent() != null && !mValue.getContent().equals("")) {
// Synthesize default option for custom value
defaultOption = new Option(mValue.getContent(), mValue.getContent());
}
if (defaultOption == null) {
binding.defaultButton.setVisibility(View.GONE);
} else {
theOptions.remove(defaultOption);
binding.defaultButton.setVisibility(View.VISIBLE);
final SVG defaultIcon = defaultOption.getIcon();
if (defaultIcon != null) {
defaultIcon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.TOP);
DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
Bitmap bitmap = Bitmap.createBitmap((int)(display.heightPixels*display.density/4), (int)(display.heightPixels*display.density/4), Bitmap.Config.ARGB_8888);
bitmap.setDensity(display.densityDpi);
Canvas bmcanvas = new Canvas(bitmap);
defaultIcon.renderToCanvas(bmcanvas);
binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
}
binding.defaultButton.setText(defaultOption.toString());
binding.defaultButton.setOnClickListener((view) -> {
mValue.setContent(defaultOption.getValue());
execute();
loading = true;
});
}
options.addAll(theOptions);
binding.buttons.setAdapter(options);
}
}
class TextFieldViewHolder extends ViewHolder<CommandTextFieldBinding> implements TextWatcher {
public TextFieldViewHolder(CommandTextFieldBinding binding) {
super(binding);
binding.textinput.addTextChangedListener(this);
}
protected Field field = null;
@Override
public void bind(Item item) {
field = (Field) item;
binding.textinputLayout.setHint(field.getLabel().or(""));
binding.textinputLayout.setHelperTextEnabled(field.getDesc().isPresent());
for (String desc : field.getDesc().asSet()) {
binding.textinputLayout.setHelperText(desc);
}
binding.textinputLayout.setErrorEnabled(field.error != null);
if (field.error != null) binding.textinputLayout.setError(field.error);
binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
String suffixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/suffix-label");
if (suffixLabel == null) {
binding.textinputLayout.setSuffixText("");
} else {
binding.textinputLayout.setSuffixText(suffixLabel);
binding.textinput.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END);
}
String prefixLabel = field.el.findChildContent("x", "https://ns.cheogram.com/prefix-label");
binding.textinputLayout.setPrefixText(prefixLabel == null ? "" : prefixLabel);
binding.textinput.setText(String.join("\n", field.getValues()));
setupInputType(field.el, binding.textinput, binding.textinputLayout);
}
@Override
public void afterTextChanged(Editable s) {
if (field == null) return;
field.setValues(List.of(s.toString().split("\n")));
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int count, int after) { }
}
class WebViewHolder extends ViewHolder<CommandWebviewBinding> {
public WebViewHolder(CommandWebviewBinding binding) { super(binding); }
protected String boundUrl = "";
@Override
public void bind(Item oob) {
setTextOrHide(binding.desc, Optional.fromNullable(oob.el.findChildContent("desc", "jabber:x:oob")));
binding.webview.getSettings().setJavaScriptEnabled(true);
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");
binding.webview.getSettings().setDatabaseEnabled(true);
binding.webview.getSettings().setDomStorageEnabled(true);
binding.webview.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
binding.progressbar.setVisibility(newProgress < 100 ? View.VISIBLE : View.GONE);
binding.progressbar.setProgress(newProgress);
}
});
binding.webview.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
mTitle = view.getTitle();
ConversationPagerAdapter.this.notifyDataSetChanged();
}
});
final String url = oob.el.findChildContent("url", "jabber:x:oob");
if (!boundUrl.equals(url)) {
binding.webview.addJavascriptInterface(new JsObject(), "xmpp_xep0050");
binding.webview.loadUrl(url);
boundUrl = url;
}
}
class JsObject {
@JavascriptInterface
public void execute() { execute("execute"); }
@JavascriptInterface
public void execute(String action) {
getView().post(() -> {
actionToWebview = null;
if(CommandSession.this.execute(action)) {
removeSession(CommandSession.this);
}
});
}
@JavascriptInterface
public void preventDefault() {
actionToWebview = binding.webview;
}
}
}
class ProgressBarViewHolder extends ViewHolder<CommandProgressBarBinding> {
public ProgressBarViewHolder(CommandProgressBarBinding binding) { super(binding); }
@Override
public void bind(Item item) {
binding.text.setVisibility(loadingHasBeenLong ? View.VISIBLE : View.GONE);
}
}
class UnknownViewHolder extends ViewHolder<CommandUnknownBinding> {
public UnknownViewHolder(CommandUnknownBinding binding) { super(binding); }
@Override
public void bind(Item item) {}
}
class Item {
protected Element el;
protected int viewType;
protected String error = null;
Item(Element el, int viewType) {
this.el = el;
this.viewType = viewType;
}
public boolean validate() {
error = null;
return true;
}
}
class Field extends Item {
Field(eu.siacs.conversations.xmpp.forms.Field el, int viewType) { super(el, viewType); }
@Override
public boolean validate() {
if (!super.validate()) return false;
if (el.findChild("required", "jabber:x:data") == null) return true;
if (getValue().getContent() != null && !getValue().getContent().equals("")) return true;
error = "this value is required";
return false;
}
public String getVar() {
return el.getAttribute("var");
}
public Optional<String> getType() {
return Optional.fromNullable(el.getAttribute("type"));
}
public Optional<String> getLabel() {
String label = el.getAttribute("label");
if (label == null) label = getVar();
return Optional.fromNullable(label);
}
public Optional<String> getDesc() {
return Optional.fromNullable(el.findChildContent("desc", "jabber:x:data"));
}
public Element getValue() {
Element value = el.findChild("value", "jabber:x:data");
if (value == null) {
value = el.addChild("value", "jabber:x:data");
}
return value;
}
public void setValues(List<String> values) {
for(Element child : el.getChildren()) {
if ("value".equals(child.getName()) && "jabber:x:data".equals(child.getNamespace())) {
el.removeChild(child);
}
}
for (String value : values) {
el.addChild("value", "jabber:x:data").setContent(value);
}
}
public List<String> getValues() {
List<String> values = new ArrayList<>();
for(Element child : el.getChildren()) {
if ("value".equals(child.getName()) && "jabber:x:data".equals(child.getNamespace())) {
values.add(child.getContent());
}
}
return values;
}
public List<Option> getOptions() {
return Option.forField(el);
}
}
class Cell extends Item {
protected Field reported;
Cell(Field reported, Element item) {
super(item, TYPE_RESULT_CELL);
this.reported = reported;
}
}
protected Field mkField(Element el) {
int viewType = TYPE_UNKNOWN;
String formType = responseElement.getAttribute("type");
if (formType != null) {
String fieldType = el.getAttribute("type");
if (fieldType == null) fieldType = "text-single";
if (formType.equals("result") || fieldType.equals("fixed")) {
viewType = TYPE_RESULT_FIELD;
} else if (formType.equals("form")) {
if (fieldType.equals("boolean")) {
if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
viewType = TYPE_BUTTON_GRID_FIELD;
} else {
viewType = TYPE_CHECKBOX_FIELD;
}
} else if (fieldType.equals("list-single")) {
Element validate = el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
if (Option.forField(el).size() > 9) {
viewType = TYPE_SEARCH_LIST_FIELD;
} else if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 1) {
viewType = TYPE_BUTTON_GRID_FIELD;
} else if (el.findChild("value", "jabber:x:data") == null || (validate != null && validate.findChild("open", "http://jabber.org/protocol/xdata-validate") != null)) {
viewType = TYPE_RADIO_EDIT_FIELD;
} else {
viewType = TYPE_SPINNER_FIELD;
}
} else {
viewType = TYPE_TEXT_FIELD;
}
}
return new Field(eu.siacs.conversations.xmpp.forms.Field.parse(el), viewType);
}
return null;
}
protected Item mkItem(Element el, int pos) {
int viewType = TYPE_UNKNOWN;
if (response != null && response.getType() == IqPacket.TYPE.RESULT) {
if (el.getName().equals("note")) {
viewType = TYPE_NOTE;
} else if (el.getNamespace().equals("jabber:x:oob")) {
viewType = TYPE_WEB;
} else if (el.getName().equals("instructions") && el.getNamespace().equals("jabber:x:data")) {
viewType = TYPE_NOTE;
} else if (el.getName().equals("field") && el.getNamespace().equals("jabber:x:data")) {
Field field = mkField(el);
if (field != null) {
items.put(pos, field);
return field;
}
}
} else if (response != null) {
viewType = TYPE_ERROR;
}
Item item = new Item(el, viewType);
items.put(pos, item);
return item;
}
class ActionsAdapter extends ArrayAdapter<Pair<String, String>> {
protected Context ctx;
public ActionsAdapter(Context ctx) {
super(ctx, R.layout.simple_list_item_selectable_text);
this.ctx = ctx;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
TextView tv = (TextView) v.findViewById(android.R.id.text1);
tv.setGravity(Gravity.CENTER);
tv.setText(getItem(position).second);
int resId = ctx.getResources().getIdentifier("action_" + getItem(position).first, "string" , ctx.getPackageName());
if (resId != 0 && getItem(position).second.equals(getItem(position).first)) tv.setText(ctx.getResources().getString(resId));
tv.setTextColor(ContextCompat.getColor(ctx, R.color.white));
tv.setBackgroundColor(UIHelper.getColorForName(getItem(position).first));
return v;
}
public int getPosition(String s) {
for(int i = 0; i < getCount(); i++) {
if (getItem(i).first.equals(s)) return i;
}
return -1;
}
public int countExceptCancel() {
int count = 0;
for(int i = 0; i < getCount(); i++) {
if (!getItem(i).first.equals("cancel")) count++;
}
return count;
}
public void clearExceptCancel() {
Pair<String,String> cancelItem = null;
for(int i = 0; i < getCount(); i++) {
if (getItem(i).first.equals("cancel")) cancelItem = getItem(i);
}
clear();
if (cancelItem != null) add(cancelItem);
}
}
final int TYPE_ERROR = 1;
final int TYPE_NOTE = 2;
final int TYPE_WEB = 3;
final int TYPE_RESULT_FIELD = 4;
final int TYPE_TEXT_FIELD = 5;
final int TYPE_CHECKBOX_FIELD = 6;
final int TYPE_SPINNER_FIELD = 7;
final int TYPE_RADIO_EDIT_FIELD = 8;
final int TYPE_RESULT_CELL = 9;
final int TYPE_PROGRESSBAR = 10;
final int TYPE_SEARCH_LIST_FIELD = 11;
final int TYPE_ITEM_CARD = 12;
final int TYPE_BUTTON_GRID_FIELD = 13;
final int TYPE_UNKNOWN = 100;
protected boolean executing = false;
protected boolean loading = false;
protected boolean loadingHasBeenLong = false;
protected Timer loadingTimer = new Timer();
protected String mTitle;
protected String mNode;
protected CommandPageBinding mBinding = null;
protected IqPacket response = null;
protected Element responseElement = null;
protected List<Field> reported = null;
protected SparseArray<Item> items = new SparseArray<>();
protected XmppConnectionService xmppConnectionService;
protected ActionsAdapter actionsAdapter;
protected GridLayoutManager layoutManager;
protected WebView actionToWebview = null;
protected int fillableFieldCount = 0;
protected IqPacket pendingResponsePacket = null;
protected boolean waitingForRefresh = false;
CommandSession(String title, String node, XmppConnectionService xmppConnectionService, Context activityContext) {
loading();
mTitle = title;
mNode = node;
this.xmppConnectionService = xmppConnectionService;
if (mPager != null) setupLayoutManager();
actionsAdapter = new ActionsAdapter(activityContext);
actionsAdapter.registerDataSetObserver(new DataSetObserver() {
@Override
public void onChanged() {
if (mBinding == null) return;
mBinding.actions.setNumColumns(actionsAdapter.getCount() > 1 ? 2 : 1);
}
@Override
public void onInvalidated() {}
});
}
public String getTitle() {
return mTitle;
}
public String getNode() {
return mNode;
}
public void updateWithResponse(final IqPacket iq) {
if (getView() != null && getView().isAttachedToWindow()) {
getView().post(() -> updateWithResponseUiThread(iq));
} else {
pendingResponsePacket = iq;
}
}
protected void updateWithResponseUiThread(final IqPacket iq) {
this.loadingTimer.cancel();
this.loadingTimer = new Timer();
this.executing = false;
this.loading = false;
this.loadingHasBeenLong = false;
this.responseElement = null;
this.fillableFieldCount = 0;
this.reported = null;
this.response = iq;
this.items.clear();
this.actionsAdapter.clear();
layoutManager.setSpanCount(1);
boolean actionsCleared = false;
Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
if (iq.getType() == IqPacket.TYPE.RESULT && command != null) {
if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) {
xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true);
}
Element actions = command.findChild("actions", "http://jabber.org/protocol/commands");
if (actions != null) {
for (Element action : actions.getChildren()) {
if (!"http://jabber.org/protocol/commands".equals(action.getNamespace())) continue;
if ("execute".equals(action.getName())) continue;
actionsAdapter.add(Pair.create(action.getName(), action.getName()));
}
}
for (Element el : command.getChildren()) {
if ("x".equals(el.getName()) && "jabber:x:data".equals(el.getNamespace())) {
Data form = Data.parse(el);
String title = form.getTitle();
if (title != null) {
mTitle = title;
ConversationPagerAdapter.this.notifyDataSetChanged();
}
if ("result".equals(el.getAttribute("type")) || "form".equals(el.getAttribute("type"))) {
this.responseElement = el;
setupReported(el.findChild("reported", "jabber:x:data"));
if (mBinding != null) mBinding.form.setLayoutManager(setupLayoutManager());
}
eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
if (actionList != null) {
actionsAdapter.clear();
for (Option action : actionList.getOptions()) {
actionsAdapter.add(Pair.create(action.getValue(), action.toString()));
}
}
String fillableFieldType = null;
String fillableFieldValue = null;
for (eu.siacs.conversations.xmpp.forms.Field field : form.getFields()) {
if ((field.getType() == null || (!field.getType().equals("hidden") && !field.getType().equals("fixed"))) && field.getFieldName() != null && !field.getFieldName().equals("http://jabber.org/protocol/commands#actions")) {
fillableFieldType = field.getType();
fillableFieldValue = field.getValue();
fillableFieldCount++;
}
}
if (fillableFieldCount == 1 && actionsAdapter.countExceptCancel() < 2 && fillableFieldType != null && (fillableFieldType.equals("list-single") || (fillableFieldType.equals("boolean") && fillableFieldValue == null))) {
actionsCleared = true;
actionsAdapter.clearExceptCancel();
}
break;
}
if (el.getName().equals("x") && el.getNamespace().equals("jabber:x:oob")) {
String url = el.findChildContent("url", "jabber:x:oob");
if (url != null) {
String scheme = Uri.parse(url).getScheme();
if (scheme.equals("http") || scheme.equals("https")) {
this.responseElement = el;
break;
}
if (scheme.equals("xmpp")) {
final Intent intent = new Intent(getView().getContext(), UriHandlerActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
getView().getContext().startActivity(intent);
break;
}
}
}
if (el.getName().equals("note") && el.getNamespace().equals("http://jabber.org/protocol/commands")) {
this.responseElement = el;
break;
}
}
if (responseElement == null && command.getAttribute("status") != null && (command.getAttribute("status").equals("completed") || command.getAttribute("status").equals("canceled"))) {
if ("jabber:iq:register".equals(mNode) && "canceled".equals(command.getAttribute("status"))) {
xmppConnectionService.archiveConversation(Conversation.this);
}
removeSession(this);
return;
}
if ("executing".equals(command.getAttribute("status")) && actionsAdapter.countExceptCancel() < 1 && !actionsCleared) {
// No actions have been given, but we are not done?
// This is probably a spec violation, but we should do *something*
actionsAdapter.add(Pair.create("execute", "execute"));
}
if (!actionsAdapter.isEmpty() || fillableFieldCount > 0) {
if ("completed".equals(command.getAttribute("status")) || "canceled".equals(command.getAttribute("status"))) {
actionsAdapter.add(Pair.create("close", "close"));
} else if (actionsAdapter.getPosition("cancel") < 0) {
actionsAdapter.insert(Pair.create("cancel", "cancel"), 0);
}
}
}
if (actionsAdapter.isEmpty()) {
actionsAdapter.add(Pair.create("close", "close"));
}
actionsAdapter.sort((x, y) -> {
if (x.first.equals("cancel")) return -1;
if (y.first.equals("cancel")) return 1;
return 0;
});
Data dataForm = null;
if (responseElement != null && responseElement.getName().equals("x") && responseElement.getNamespace().equals("jabber:x:data")) dataForm = Data.parse(responseElement);
if (mNode.equals("jabber:iq:register") &&
xmppConnectionService.getPreferences().contains("onboarding_action") &&
dataForm != null && dataForm.getFieldByName("gateway-jid") != null) {
dataForm.put("gateway-jid", xmppConnectionService.getPreferences().getString("onboarding_action", ""));
execute();
}
xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
notifyDataSetChanged();
}
protected void setupReported(Element el) {
if (el == null) {
reported = null;
return;
}
reported = new ArrayList<>();
for (Element fieldEl : el.getChildren()) {
if (!fieldEl.getName().equals("field") || !fieldEl.getNamespace().equals("jabber:x:data")) continue;
reported.add(mkField(fieldEl));
}
}
@Override
public int getItemCount() {
if (loading) return 1;
if (response == null) return 0;
if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) {
int i = 0;
for (Element el : responseElement.getChildren()) {
if (!el.getNamespace().equals("jabber:x:data")) continue;
if (el.getName().equals("title")) continue;
if (el.getName().equals("field")) {
String type = el.getAttribute("type");
if (type != null && type.equals("hidden")) continue;
if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
}
if (el.getName().equals("reported") || el.getName().equals("item")) {
if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
if (el.getName().equals("reported")) continue;
i += 1;
} else {
if (reported != null) i += reported.size();
}
continue;
}
i++;
}
return i;
}
return 1;
}
public Item getItem(int position) {
if (loading) return new Item(null, TYPE_PROGRESSBAR);
if (items.get(position) != null) return items.get(position);
if (response == null) return null;
if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) {
if (responseElement.getNamespace().equals("jabber:x:data")) {
int i = 0;
for (Element el : responseElement.getChildren()) {
if (!el.getNamespace().equals("jabber:x:data")) continue;
if (el.getName().equals("title")) continue;
if (el.getName().equals("field")) {
String type = el.getAttribute("type");
if (type != null && type.equals("hidden")) continue;
if (el.getAttribute("var") != null && el.getAttribute("var").equals("http://jabber.org/protocol/commands#actions")) continue;
}
if (el.getName().equals("reported") || el.getName().equals("item")) {
Cell cell = null;
if (reported != null) {
if ((layoutManager == null ? 1 : layoutManager.getSpanCount()) < reported.size()) {
if (el.getName().equals("reported")) continue;
if (i == position) {
items.put(position, new Item(el, TYPE_ITEM_CARD));
return items.get(position);
}
} else {
if (reported.size() > position - i) {
Field reportedField = reported.get(position - i);
Element itemField = null;
if (el.getName().equals("item")) {
for (Element subel : el.getChildren()) {
if (subel.getAttribute("var").equals(reportedField.getVar())) {
itemField = subel;
break;
}
}
}
cell = new Cell(reportedField, itemField);
} else {
i += reported.size();
continue;
}
}
}
if (cell != null) {
items.put(position, cell);
return cell;
}
}
if (i < position) {
i++;
continue;
}
return mkItem(el, position);
}
}
}
return mkItem(responseElement == null ? response : responseElement, position);
}
@Override
public int getItemViewType(int position) {
return getItem(position).viewType;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup container, int viewType) {
switch(viewType) {
case TYPE_ERROR: {
CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
return new ErrorViewHolder(binding);
}
case TYPE_NOTE: {
CommandNoteBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_note, container, false);
return new NoteViewHolder(binding);
}
case TYPE_WEB: {
CommandWebviewBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_webview, container, false);
return new WebViewHolder(binding);
}
case TYPE_RESULT_FIELD: {
CommandResultFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_field, container, false);
return new ResultFieldViewHolder(binding);
}
case TYPE_RESULT_CELL: {
CommandResultCellBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_result_cell, container, false);
return new ResultCellViewHolder(binding);
}
case TYPE_ITEM_CARD: {
CommandItemCardBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_item_card, container, false);
return new ItemCardViewHolder(binding);
}
case TYPE_CHECKBOX_FIELD: {
CommandCheckboxFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_checkbox_field, container, false);
return new CheckboxFieldViewHolder(binding);
}
case TYPE_SEARCH_LIST_FIELD: {
CommandSearchListFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_search_list_field, container, false);
return new SearchListFieldViewHolder(binding);
}
case TYPE_RADIO_EDIT_FIELD: {
CommandRadioEditFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_radio_edit_field, container, false);
return new RadioEditFieldViewHolder(binding);
}
case TYPE_SPINNER_FIELD: {
CommandSpinnerFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_spinner_field, container, false);
return new SpinnerFieldViewHolder(binding);
}
case TYPE_BUTTON_GRID_FIELD: {
CommandButtonGridFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_button_grid_field, container, false);
return new ButtonGridFieldViewHolder(binding);
}
case TYPE_TEXT_FIELD: {
CommandTextFieldBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_text_field, container, false);
return new TextFieldViewHolder(binding);
}
case TYPE_PROGRESSBAR: {
CommandProgressBarBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_progress_bar, container, false);
return new ProgressBarViewHolder(binding);
}
case TYPE_UNKNOWN: {
CommandUnknownBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_unknown, container, false);
return new UnknownViewHolder(binding);
}
default:
throw new IllegalArgumentException("Unknown viewType: " + viewType + " based on: " + response);
}
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
viewHolder.bind(getItem(position));
}
public View getView() {
if (mBinding == null) return null;
return mBinding.getRoot();
}
public boolean validate() {
int count = getItemCount();
boolean isValid = true;
for (int i = 0; i < count; i++) {
boolean oneIsValid = getItem(i).validate();
isValid = isValid && oneIsValid;
}
notifyDataSetChanged();
return isValid;
}
public boolean execute() {
return execute("execute");
}
public boolean execute(int actionPosition) {
return execute(actionsAdapter.getItem(actionPosition).first);
}
public synchronized boolean execute(String action) {
if (!"cancel".equals(action) && executing) {
loadingHasBeenLong = true;
notifyDataSetChanged();
return false;
}
if (!action.equals("cancel") && !action.equals("prev") && !validate()) return false;
if (response == null) return true;
Element command = response.findChild("command", "http://jabber.org/protocol/commands");
if (command == null) return true;
String status = command.getAttribute("status");
if (status == null || (!status.equals("executing") && !action.equals("prev"))) return true;
if (actionToWebview != null && !action.equals("cancel") && Build.VERSION.SDK_INT >= 23) {
actionToWebview.postWebMessage(new WebMessage("xmpp_xep0050/" + action), Uri.parse("*"));
return false;
}
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(response.getFrom());
final Element c = packet.addChild("command", Namespace.COMMANDS);
c.setAttribute("node", mNode);
c.setAttribute("sessionid", command.getAttribute("sessionid"));
String formType = responseElement == null ? null : responseElement.getAttribute("type");
if (!action.equals("cancel") &&
!action.equals("prev") &&
responseElement != null &&
responseElement.getName().equals("x") &&
responseElement.getNamespace().equals("jabber:x:data") &&
formType != null && formType.equals("form")) {
Data form = Data.parse(responseElement);
eu.siacs.conversations.xmpp.forms.Field actionList = form.getFieldByName("http://jabber.org/protocol/commands#actions");
if (actionList != null) {
actionList.setValue(action);
c.setAttribute("action", "execute");
}
responseElement.setAttribute("type", "submit");
Element rsm = responseElement.findChild("set", "http://jabber.org/protocol/rsm");
if (rsm != null) {
Element max = new Element("max", "http://jabber.org/protocol/rsm");
max.setContent("1000");
rsm.addChild(max);
}
c.addChild(responseElement);
}
if (c.getAttribute("action") == null) c.setAttribute("action", action);
executing = true;
xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> {
updateWithResponse(iq);
}, 120L);
loading();
return false;
}
public void refresh() {
synchronized(this) {
if (waitingForRefresh) notifyDataSetChanged();
}
}
protected void loading() {
View v = getView();
try {
loadingTimer.schedule(new TimerTask() {
@Override
public void run() {
View v2 = getView();
loading = true;
loadingTimer.schedule(new TimerTask() {
@Override
public void run() {
loadingHasBeenLong = true;
if (v == null && v2 == null) return;
(v == null ? v2 : v).post(() -> notifyDataSetChanged());
}
}, 3000);
if (v == null && v2 == null) return;
(v == null ? v2 : v).post(() -> notifyDataSetChanged());
}
}, 500);
} catch (final IllegalStateException e) { }
}
protected GridLayoutManager setupLayoutManager() {
int spanCount = 1;
Context ctx = mPager == null ? getView().getContext() : mPager.getContext();
if (reported != null) {
float screenWidth = ctx.getResources().getDisplayMetrics().widthPixels;
TextPaint paint = ((TextView) LayoutInflater.from(mPager.getContext()).inflate(R.layout.command_result_cell, null)).getPaint();
float tableHeaderWidth = reported.stream().reduce(
0f,
(total, field) -> total + StaticLayout.getDesiredWidth(field.getLabel().or("--------") + "\t", paint),
(a, b) -> a + b
);
spanCount = tableHeaderWidth > 0.59 * screenWidth ? 1 : this.reported.size();
}
if (layoutManager != null && layoutManager.getSpanCount() != spanCount) {
items.clear();
notifyDataSetChanged();
}
layoutManager = new GridLayoutManager(ctx, spanCount);
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (getItemViewType(position) != TYPE_RESULT_CELL) return layoutManager.getSpanCount();
return 1;
}
});
return layoutManager;
}
protected void setBinding(CommandPageBinding b) {
mBinding = b;
// https://stackoverflow.com/a/32350474/8611
mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
if(rv.getChildCount() > 0) {
int[] location = new int[2];
rv.getLocationOnScreen(location);
View childView = rv.findChildViewUnder(e.getX(), e.getY());
if (childView instanceof ViewGroup) {
childView = findViewAt((ViewGroup) childView, location[0] + e.getX(), location[1] + e.getY());
}
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(1)) || childView instanceof WebView) {
rv.requestDisallowInterceptTouchEvent(true);
}
case MotionEvent.ACTION_UP:
if ((childView instanceof AbsListView && ((AbsListView) childView).canScrollList(-11)) || childView instanceof WebView) {
rv.requestDisallowInterceptTouchEvent(true);
}
}
}
return false;
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallow) { }
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) { }
});
mBinding.form.setLayoutManager(setupLayoutManager());
mBinding.form.setAdapter(this);
mBinding.actions.setAdapter(actionsAdapter);
mBinding.actions.setOnItemClickListener((parent, v, pos, id) -> {
if (execute(pos)) {
removeSession(CommandSession.this);
}
});
actionsAdapter.notifyDataSetChanged();
if (pendingResponsePacket != null) {
final IqPacket pending = pendingResponsePacket;
pendingResponsePacket = null;
updateWithResponseUiThread(pending);
}
}
public View inflateUi(Context context, Consumer<ConversationPage> remover) {
CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.command_page, null, false);
setBinding(binding);
return binding.getRoot();
}
// https://stackoverflow.com/a/36037991/8611
private View findViewAt(ViewGroup viewGroup, float x, float y) {
for(int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup && !(child instanceof AbsListView) && !(child instanceof WebView)) {
View foundView = findViewAt((ViewGroup) child, x, y);
if (foundView != null && foundView.isShown()) {
return foundView;
}
} else {
int[] location = new int[2];
child.getLocationOnScreen(location);
Rect rect = new Rect(location[0], location[1], location[0] + child.getWidth(), location[1] + child.getHeight());
if (rect.contains((int)x, (int)y)) {
return child;
}
}
}
return null;
}
}
}
}