Fetch and render vcard4

Stephen Paul Weber created

Change summary

src/cheogram/java/com/cheogram/android/Util.java                         |  28 
src/cheogram/res/drawable/business_black.xml                             |  10 
src/cheogram/res/drawable/business_white.xml                             |  10 
src/cheogram/res/drawable/email_black.xml                                |  10 
src/cheogram/res/drawable/email_white.xml                                |  10 
src/cheogram/res/drawable/ic_chat_black_24dp.xml                         |  11 
src/cheogram/res/drawable/jabber.xml                                     |  15 
src/cheogram/res/drawable/link_black.xml                                 |  10 
src/cheogram/res/drawable/link_white.xml                                 |  10 
src/cheogram/res/values/attrs.xml                                        |   7 
src/cheogram/res/values/themes.xml                                       |   8 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |   6 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  29 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      | 112 
src/main/java/eu/siacs/conversations/xml/Namespace.java                  |   1 
src/main/res/layout/activity_contact_details.xml                         |  18 
16 files changed, 295 insertions(+)

Detailed changes

src/cheogram/java/com/cheogram/android/Util.java 🔗

@@ -0,0 +1,28 @@
+package com.cheogram.android;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.ListAdapter;
+
+public class Util {
+	public static 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();
+	}
+}

src/cheogram/res/drawable/business_black.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@color/black54"
+      android:pathData="M12,7L12,3L2,3v18h20L22,7L12,7zM6,19L4,19v-2h2v2zM6,15L4,15v-2h2v2zM6,11L4,11L4,9h2v2zM6,7L4,7L4,5h2v2zM10,19L8,19v-2h2v2zM10,15L8,15v-2h2v2zM10,11L8,11L8,9h2v2zM10,7L8,7L8,5h2v2zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2L12,9h8v10zM18,11h-2v2h2v-2zM18,15h-2v2h2v-2z"/>
+</vector>

src/cheogram/res/drawable/business_white.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,7L12,3L2,3v18h20L22,7L12,7zM6,19L4,19v-2h2v2zM6,15L4,15v-2h2v2zM6,11L4,11L4,9h2v2zM6,7L4,7L4,5h2v2zM10,19L8,19v-2h2v2zM10,15L8,15v-2h2v2zM10,11L8,11L8,9h2v2zM10,7L8,7L8,5h2v2zM20,19h-8v-2h2v-2h-2v-2h2v-2h-2L12,9h8v10zM18,11h-2v2h2v-2zM18,15h-2v2h2v-2z"/>
+</vector>

src/cheogram/res/drawable/email_black.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@color/black54"
+      android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
+</vector>

src/cheogram/res/drawable/email_white.xml 🔗

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
+</vector>

src/cheogram/res/drawable/ic_chat_black_24dp.xml 🔗

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="@color/black54"
+      android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
+</vector>

src/cheogram/res/drawable/jabber.xml 🔗

@@ -0,0 +1,36 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
+    android:viewportWidth="54.67484"
+    android:viewportHeight="69.64137"
+    android:width="18.8422dp"
+    android:height="24dp">
+    <path
+        android:pathData="M-69.123113 1078.1423Z"
+        android:fillColor="#000000"
+        android:strokeMiterLimit="10" />
+    <group
+        android:scaleX="0.02688812"
+        android:scaleY="-0.02688812"
+        android:translateX="-4.373626"
+        android:translateY="98.6128">
+        <path
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@color/black54"
+      android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
+</vector>
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
+</vector>

src/cheogram/res/values/attrs.xml 🔗

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <attr name="icon_email" format="reference" />
+    <attr name="icon_link" format="reference" />
+    <attr name="icon_org" format="reference" />
+    <attr name="icon_chat" format="reference" />
+</resources>

src/cheogram/res/values/themes.xml 🔗

@@ -97,6 +97,10 @@
         <item name="media_preview_backup" type="reference">@drawable/ic_backup_black_48dp</item>
         <item name="media_preview_unknown" type="reference">@drawable/ic_help_black_48dp</item>
 
+        <item name="icon_link" type="reference">@drawable/link_black</item>
+        <item name="icon_email" type="reference">@drawable/email_black</item>
+        <item name="icon_org" type="reference">@drawable/business_black</item>
+        <item name="icon_chat" type="reference">@drawable/ic_chat_black_24dp</item>
         <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item>
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_black_24dp</item>
@@ -252,6 +256,10 @@
         <item name="media_preview_backup" type="reference">@drawable/ic_backup_white_48dp</item>
         <item name="media_preview_unknown" type="reference">@drawable/ic_help_white_48dp</item>
 
+        <item name="icon_link" type="reference">@drawable/link_white</item>
+        <item name="icon_email" type="reference">@drawable/email_white</item>
+        <item name="icon_org" type="reference">@drawable/business_white</item>
+        <item name="icon_chat" type="reference">@drawable/ic_chat_white_24dp</item>
         <item name="icon_add_group" type="reference">@drawable/ic_group_add_white_24dp</item>
         <item name="icon_add_person" type="reference">@drawable/ic_person_add_white_24dp</item>
         <item name="icon_cancel" type="reference">@drawable/ic_cancel_white_24dp</item>

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -135,6 +135,12 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
+    public IqPacket retrieveVcard4(final Jid jid) {
+        final IqPacket packet = retrieve("urn:xmpp:vcard4", null);
+        packet.setTo(jid);
+        return packet;
+    }
+
     public IqPacket retrieveBookmarks() {
         return retrieve(Namespace.BOOKMARKS2, null);
     }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -4152,6 +4152,35 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
+        IqPacket packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
+        sendIqPacket(account, packet, (a, result) -> {
+            if (result.getType() == IqPacket.TYPE.RESULT) {
+                final Element item = mIqParser.getItem(result);
+                if (item != null) {
+                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
+                    if (vcard4 != null) {
+                        if (callback != null) {
+                            callback.accept(vcard4);
+                        }
+                        return;
+                    }
+                }
+            } else {
+                Element error = result.findChild("error");
+                if (error == null) {
+                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
+                } else {
+                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
+                }
+            }
+            if (callback != null) {
+                callback.accept(null);
+            }
+
+        });
+    }
+
     public void deleteContactOnServer(Contact contact) {
         contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
         contact.resetOption(Contact.Options.DIRTY_PUSH);

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -6,6 +6,7 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -16,11 +17,13 @@ import android.provider.ContactsContract.Intents;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.style.RelativeSizeSpan;
+import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
@@ -32,6 +35,8 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 
+import com.cheogram.android.Util;
+
 import org.openintents.openpgp.util.OpenPgpUtils;
 
 import java.util.ArrayList;
@@ -48,6 +53,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
+import eu.siacs.conversations.databinding.CommandRowBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
@@ -62,6 +68,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
 import eu.siacs.conversations.ui.util.GridManager;
 import eu.siacs.conversations.ui.util.JidDialog;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
+import eu.siacs.conversations.ui.util.ShareUtil;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.Emoticons;
@@ -69,6 +76,7 @@ import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
@@ -622,6 +630,47 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
                 this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact));
             }
+
+            final VcardAdapter items = new VcardAdapter();
+            binding.profileItems.setAdapter(items);
+            binding.profileItems.setOnItemClickListener((a0, v, pos, a3) -> {
+                final Uri uri = items.getUri(pos);
+                if (uri == null) return;
+
+                if (uri.getScheme().equals("xmpp")) {
+                    switchToConversation(xmppConnectionService.findOrCreateConversation(account, Jid.of(uri.getSchemeSpecificPart()), false, true));
+                } else {
+                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+                    startActivity(intent);
+                }
+            });
+            binding.profileItems.setOnItemLongClickListener((a0, v, pos, a3) -> {
+                String toCopy = null;
+                final Uri uri = items.getUri(pos);
+                if (uri != null) toCopy = uri.toString();
+                if (toCopy == null) {
+                    toCopy = items.getItem(pos).findChildContent("text", Namespace.VCARD4);
+                }
+
+                if (toCopy == null) return false;
+                if (ShareUtil.copyTextToClipboard(ContactDetailsActivity.this, toCopy, R.string.message)) {
+                    Toast.makeText(ContactDetailsActivity.this, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show();
+                }
+                return true;
+            });
+            xmppConnectionService.fetchVcard4(account, contact, (vcard4) -> {
+                if (vcard4 == null) return;
+
+                runOnUiThread(() -> {
+                    for (Element el : vcard4.getChildren()) {
+                        if (el.findChildEnsureSingle("uri", Namespace.VCARD4) != null || el.findChildEnsureSingle("text", Namespace.VCARD4) != null) {
+                            items.add(el);
+                        }
+                    }
+                    Util.justifyListViewHeightBasedOnChildren(binding.profileItems);
+                });
+            });
+
             populateView();
         }
     }
@@ -651,4 +700,67 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         });
 
     }
+
+    class VcardAdapter extends ArrayAdapter<Element> {
+        VcardAdapter() { super(ContactDetailsActivity.this, 0); }
+
+        private Drawable getDrawable(int attr) {
+            final TypedValue typedvalueattr = new TypedValue();
+            getTheme().resolveAttribute(attr, typedvalueattr, true);
+            return getResources().getDrawable(typedvalueattr.resourceId);
+        }
+
+        @Override
+        public View getView(int position, View view, @NonNull ViewGroup parent) {
+            final CommandRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.command_row, parent, false);
+            final Element item = getItem(position);
+
+            if (item.getName().equals("org")) {
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_org), null, null, null);
+                binding.command.setCompoundDrawablePadding(20);
+            } else if (item.getName().equals("impp")) {
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_chat), null, null, null);
+                binding.command.setCompoundDrawablePadding(20);
+            } else if (item.getName().equals("url")) {
+                binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_link), null, null, null);
+                binding.command.setCompoundDrawablePadding(20);
+            }
+
+            final Uri uri = getUri(position);
+            if (uri != null) {
+                if (uri.getScheme().equals("xmpp")) {
+                    binding.command.setText(uri.getSchemeSpecificPart());
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getResources().getDrawable(R.drawable.jabber), null, null, null);
+                    binding.command.setCompoundDrawablePadding(20);
+                } else if (uri.getScheme().equals("tel")) {
+                    binding.command.setText(uri.getSchemeSpecificPart());
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.ic_make_audio_call), null, null, null);
+                    binding.command.setCompoundDrawablePadding(20);
+                } else if (uri.getScheme().equals("mailto")) {
+                    binding.command.setText(uri.getSchemeSpecificPart());
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_email), null, null, null);
+                    binding.command.setCompoundDrawablePadding(20);
+                } else if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
+                    binding.command.setText(uri.toString());
+                    binding.command.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawable(R.attr.icon_link), null, null, null);
+                    binding.command.setCompoundDrawablePadding(20);
+                } else {
+                    binding.command.setText(uri.toString());
+                }
+            } else {
+                final String text = item.findChildContent("text", Namespace.VCARD4);
+                binding.command.setText(text);
+            }
+
+            return binding.getRoot();
+        }
+
+        public Uri getUri(int pos) {
+            final Element item = getItem(pos);
+            final String uriS = item.findChildContent("uri", Namespace.VCARD4);
+            if (uriS != null) return Uri.parse(uriS).normalizeScheme();
+            if (item.getName().equals("email")) return Uri.parse("mailto:" + item.findChildContent("text", Namespace.VCARD4));
+            return null;
+        }
+    }
 }

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -66,4 +66,5 @@ public final class Namespace {
     public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite";
     public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
     public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push";
+    public static final String VCARD4 = "urn:ietf:params:xml:ns:vcard-4.0";
 }

src/main/res/layout/activity_contact_details.xml 🔗

@@ -128,6 +128,24 @@
                     </RelativeLayout>
                 </androidx.cardview.widget.CardView>
 
+                <androidx.cardview.widget.CardView
+                    android:id="@+id/profile"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
+                    android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginTop="@dimen/activity_vertical_margin">
+
+                    <ListView
+                        android:id="@+id/profile_items"
+                        android:layout_width="fill_parent"
+                        android:layout_height="wrap_content"
+                        android:divider="@android:color/transparent"
+                        android:dividerHeight="0dp"></ListView>
+
+                </androidx.cardview.widget.CardView>
+
                 <androidx.cardview.widget.CardView
                     android:id="@+id/media_wrapper"
                     android:layout_width="fill_parent"