From f2012bc7f5fb4ef8c342f7a6e2e1cf6843957a25 Mon Sep 17 00:00:00 2001 From: kosyak Date: Wed, 25 Oct 2023 23:38:54 +0200 Subject: [PATCH] commands tab in conversation --- build.gradle | 2 + .../siacs/conversations/entities/Contact.java | 7 + .../conversations/entities/Conversation.java | 1932 +++++++++++++++++ .../conversations/entities/Presence.java | 2 +- .../conversations/entities/Presences.java | 13 + .../conversations/generator/IqGenerator.java | 9 +- .../persistance/FileBackend.java | 113 + .../services/XmppConnectionService.java | 77 +- .../ui/ConversationFragment.java | 156 ++ .../ui/ConversationsActivity.java | 57 +- .../siacs/conversations/ui/XmppActivity.java | 11 + .../siacs/conversations/ui/XmppFragment.java | 2 + .../ui/adapter/CommandAdapter.java | 27 + .../conversations/ui/text/FixedURLSpan.java | 19 +- .../conversations/ui/util/ShareUtil.java | 30 + .../conversations/ui/widget/GridView.java | 37 + .../conversations/ui/widget/TabLayout.java | 40 + .../conversations/xmpp/XmppConnection.java | 44 +- .../siacs/conversations/xmpp/forms/Field.java | 4 + .../conversations/xmpp/forms/Option.java | 66 + src/main/res/drawable/list_choice.xml | 9 + .../res/layout/activity_conversations.xml | 2 +- src/main/res/layout/button_grid_item.xml | 12 + src/main/res/layout/command_button.xml | 8 + .../res/layout/command_button_grid_field.xml | 67 + .../res/layout/command_checkbox_field.xml | 49 + src/main/res/layout/command_item_card.xml | 22 + src/main/res/layout/command_note.xml | 31 + src/main/res/layout/command_page.xml | 30 + src/main/res/layout/command_progress_bar.xml | 34 + .../res/layout/command_radio_edit_field.xml | 53 + src/main/res/layout/command_result_cell.xml | 16 + src/main/res/layout/command_result_field.xml | 53 + src/main/res/layout/command_row.xml | 17 + .../res/layout/command_search_list_field.xml | 47 + src/main/res/layout/command_spinner_field.xml | 38 + src/main/res/layout/command_text_field.xml | 30 + src/main/res/layout/command_webview.xml | 36 + src/main/res/layout/fragment_conversation.xml | 434 ++-- src/main/res/layout/radio_grid_item.xml | 9 + src/main/res/layout/simple_list_item.xml | 3 +- src/main/res/menu/fragment_conversation.xml | 6 + src/main/res/values/strings.xml | 2 + 43 files changed, 3434 insertions(+), 222 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/CommandAdapter.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/GridView.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/TabLayout.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/forms/Option.java create mode 100644 src/main/res/drawable/list_choice.xml create mode 100644 src/main/res/layout/button_grid_item.xml create mode 100644 src/main/res/layout/command_button.xml create mode 100644 src/main/res/layout/command_button_grid_field.xml create mode 100644 src/main/res/layout/command_checkbox_field.xml create mode 100644 src/main/res/layout/command_item_card.xml create mode 100644 src/main/res/layout/command_note.xml create mode 100644 src/main/res/layout/command_page.xml create mode 100644 src/main/res/layout/command_progress_bar.xml create mode 100644 src/main/res/layout/command_radio_edit_field.xml create mode 100644 src/main/res/layout/command_result_cell.xml create mode 100644 src/main/res/layout/command_result_field.xml create mode 100644 src/main/res/layout/command_row.xml create mode 100644 src/main/res/layout/command_search_list_field.xml create mode 100644 src/main/res/layout/command_spinner_field.xml create mode 100644 src/main/res/layout/command_text_field.xml create mode 100644 src/main/res/layout/command_webview.xml create mode 100644 src/main/res/layout/radio_grid_item.xml diff --git a/build.gradle b/build.gradle index 21883b18a..5eba51800 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,8 @@ dependencies { implementation 'com.splitwise:tokenautocomplete:3.0.2' implementation 'com.github.kizitonwose.colorpreference:support:1.1.0' + implementation 'com.caverock:androidsvg-aar:1.4' + implementation 'com.github.singpolyma:Better-Link-Movement-Method:4df081e1e4' } ext { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index e889be11e..7b530300f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -294,6 +294,13 @@ public class Contact implements ListItem, Blockable { return this.presences.getShownStatus(); } + public Jid resourceWhichSupport(final String namespace) { + final String resource = getPresences().firstWhichSupport(namespace); + if (resource == null) return null; + + return resource.equals("") ? getJid() : getJid().withResource(resource); + } + public boolean setPhotoUri(String uri) { if (uri != null && !uri.equals(this.photoUri)) { this.photoUri = uri; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index e5592609f..e1bd8fc2f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1,12 +1,69 @@ 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.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; @@ -15,26 +72,63 @@ 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.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Objects; +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.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; @@ -88,6 +182,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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 parentConversation = null; @@ -1202,6 +1298,41 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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); } @@ -1223,4 +1354,1805 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return message; } } + + public interface ConversationPage { + public String getTitle(); + public String getNode(); + public View inflateUi(Context context, Consumer remover); + public View getView(); + public void refresh(); + } + + public class ConversationPagerAdapter extends PagerAdapter { + protected ViewPager mPager = null; + protected TabLayout mTabs = null; + ArrayList 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 implements ConversationPage { + abstract class ViewHolder 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 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 { + 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 { + 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 { + 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