basic phone number sync

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java          |   2 
src/main/java/eu/siacs/conversations/entities/Contact.java                      |  31 
src/main/java/eu/siacs/conversations/entities/Roster.java                       |   6 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java        |  10 
src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java | 108 
src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java       |   8 
6 files changed, 133 insertions(+), 32 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -20,6 +20,8 @@ import java.util.Locale;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.AbstractPhoneContact;
+import eu.siacs.conversations.android.PhoneNumberContact;
 import eu.siacs.conversations.utils.JidHelper;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xml.Element;
@@ -507,6 +509,33 @@ public class Contact implements ListItem, Blockable {
 		return serverName;
 	}
 
+	public synchronized boolean setPhoneContact(AbstractPhoneContact phoneContact) {
+		setOption(getOption(phoneContact.getClass()));
+		setSystemAccount(phoneContact.getLookupUri());
+		boolean changed = setSystemName(phoneContact.getDisplayName());
+		changed |= setPhotoUri(phoneContact.getPhotoUri());
+		return changed;
+	}
+
+	public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
+		resetOption(getOption(clazz));
+		boolean changed = false;
+		if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
+			setSystemAccount(null);
+			changed |= setPhotoUri(null);
+			changed |= setSystemName(null);
+		}
+		return changed;
+	}
+
+	public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
+		if (clazz == PhoneNumberContact.class) {
+			return Options.SYNCED_VIA_ADDRESSBOOK;
+		} else {
+			return Options.SYNCED_VIA_OTHER;
+		}
+	}
+
     public final class Options {
 		public static final int TO = 0;
 		public static final int FROM = 1;
@@ -516,5 +545,7 @@ public class Contact implements ListItem, Blockable {
 		public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
 		public static final int DIRTY_PUSH = 6;
 		public static final int DIRTY_DELETE = 7;
+		private static final int SYNCED_VIA_ADDRESSBOOK = 8;
+		private static final int SYNCED_VIA_OTHER = 9;
 	}
 }

src/main/java/eu/siacs/conversations/entities/Roster.java 🔗

@@ -5,6 +5,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 
+import eu.siacs.conversations.android.AbstractPhoneContact;
 import rocks.xmpp.addr.Jid;
 
 
@@ -55,11 +56,12 @@ public class Roster {
 		}
 	}
 
-	public List<Contact> getWithSystemAccounts() {
+	public List<Contact> getWithSystemAccounts(Class<?extends AbstractPhoneContact> clazz) {
+		int option = Contact.getOption(clazz);
 		List<Contact> with = getContacts();
 		for(Iterator<Contact> iterator = with.iterator(); iterator.hasNext();) {
 			Contact contact = iterator.next();
-			if (contact.getSystemAccount() == null) {
+			if (!contact.getOption(option)) {
 				iterator.remove();
 			}
 		}

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

@@ -1523,21 +1523,17 @@ public class XmppConnectionService extends Service {
             Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
             Log.d(Config.LOGTAG, "start merging phone contacts with roster");
             for (Account account : accounts) {
-                List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
+                List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class);
                 for (JabberIdContact jidContact : contacts.values()) {
                     final Contact contact = account.getRoster().getContact(jidContact.getJid());
-                    contact.setSystemAccount(jidContact.getLookupUri());
-                    boolean needsCacheClean = contact.setPhotoUri(jidContact.getPhotoUri());
-                    needsCacheClean |= contact.setSystemName(jidContact.getDisplayName());
+                    boolean needsCacheClean = contact.setPhoneContact(jidContact);
                     if (needsCacheClean) {
                         getAvatarService().clear(contact);
                     }
                     withSystemAccounts.remove(contact);
                 }
                 for (Contact contact : withSystemAccounts) {
-                    contact.setSystemAccount(null);
-                    boolean needsCacheClean = contact.setPhotoUri(null);
-                    needsCacheClean |= contact.setSystemName(null);
+                    boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class);
                     if (needsCacheClean) {
                         getAvatarService().clear(contact);
                     }

src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java 🔗

@@ -27,14 +27,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.net.ssl.SSLHandshakeException;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.JabberIdContact;
 import eu.siacs.conversations.android.PhoneNumberContact;
 import eu.siacs.conversations.crypto.sasl.Plain;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import io.michaelrocks.libphonenumber.android.Phonenumber;
@@ -63,6 +66,14 @@ public class QuickConversationsService extends AbstractQuickConversationsService
         super(xmppConnectionService);
     }
 
+    private static long retryAfter(HttpURLConnection connection) {
+        try {
+            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
     public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
         synchronized (mOnVerificationRequested) {
             mOnVerificationRequested.add(onVerificationRequested);
@@ -246,14 +257,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService
         }
     }
 
-    private static long retryAfter(HttpURLConnection connection) {
-        try {
-            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
-        } catch (Exception e) {
-            return 0;
-        }
-    }
-
     public boolean isVerifying() {
         return mVerificationInProgress.get();
     }
@@ -265,29 +268,94 @@ public class QuickConversationsService extends AbstractQuickConversationsService
     @Override
     public void considerSync() {
         Map<String, PhoneNumberContact> contacts = PhoneNumberContact.load(service);
-        for(Account account : service.getAccounts()) {
+        for (Account account : service.getAccounts()) {
             considerSync(account, contacts);
         }
     }
 
-    private void considerSync(Account account, Map<String, PhoneNumberContact> contacts) {
+    private void considerSync(Account account, final Map<String, PhoneNumberContact> contacts) {
         XmppConnection xmppConnection = account.getXmppConnection();
         Jid syncServer = xmppConnection == null ? null : xmppConnection.findDiscoItemByFeature(Namespace.SYNCHRONIZATION);
         if (syncServer == null) {
-            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": skipping sync. no sync server found");
+            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping sync. no sync server found");
             return;
         }
-        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": sending phone list to "+syncServer);
+        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer);
         List<Element> entries = new ArrayList<>();
-        for(PhoneNumberContact c : contacts.values()) {
-            entries.add(new Element("entry").setAttribute("number",c.getPhoneNumber()));
+        for (PhoneNumberContact c : contacts.values()) {
+            entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber()));
+        }
+        IqPacket query = new IqPacket(IqPacket.TYPE.GET);
+        query.setTo(syncServer);
+        query.addChild(new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries));
+        service.sendIqPacket(account, query, (a, response) -> {
+            if (response.getType() == IqPacket.TYPE.RESULT) {
+                List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
+                final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION);
+                if (phoneBook != null) {
+                    for(Entry entry : Entry.ofPhoneBook(phoneBook)) {
+                        PhoneNumberContact phoneContact = contacts.get(entry.getNumber());
+                        if (phoneContact == null) {
+                            continue;
+                        }
+                        for(Jid jid : entry.getJids()) {
+                            Contact contact = account.getRoster().getContact(jid);
+                            final boolean needsCacheClean = contact.setPhoneContact(phoneContact);
+                            if (needsCacheClean) {
+                                service.getAvatarService().clear(contact);
+                            }
+                            withSystemAccounts.remove(contact);
+                        }
+                    }
+                }
+                for (Contact contact : withSystemAccounts) {
+                    boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class);
+                    if (needsCacheClean) {
+                        service.getAvatarService().clear(contact);
+                    }
+                }
+            }
+        });
+    }
+
+    public static class Entry {
+        private final List<Jid> jids;
+        private final String number;
+
+        private Entry(String number, List<Jid> jids) {
+            this.number = number;
+            this.jids = jids;
+        }
+
+        public static Entry of(Element element) {
+            final String number = element.getAttribute("number");
+            final List<Jid> jids = new ArrayList<>();
+            for (Element jidElement : element.getChildren()) {
+                String content = jidElement.getContent();
+                if (content != null) {
+                    jids.add(Jid.of(content));
+                }
+            }
+            return new Entry(number, jids);
+        }
+
+        public static List<Entry> ofPhoneBook(Element phoneBook) {
+            List<Entry> entries = new ArrayList<>();
+            for (Element entry : phoneBook.getChildren()) {
+                if ("entry".equals(entry.getName())) {
+                    entries.add(of(entry));
+                }
+            }
+            return entries;
+        }
+
+        public List<Jid> getJids() {
+            return jids;
+        }
+
+        public String getNumber() {
+            return number;
         }
-        Element phoneBook = new Element("phone-book",Namespace.SYNCHRONIZATION);
-        phoneBook.setChildren(entries);
-        IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
-        iqPacket.setTo(syncServer);
-        iqPacket.addChild(phoneBook);
-        service.sendIqPacket(account, iqPacket, null);
     }
 
     public interface OnVerificationRequested {

src/quicksy/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java 🔗

@@ -53,8 +53,12 @@ public class PhoneNumberUtilWrapper {
         return getInstance(context).parse(jid.getEscapedLocal(), "de");
     }
 
-    public static String normalize(Context context, String number) throws NumberParseException {
-        return normalize(context, getInstance(context).parse(number, getUserCountry(context)));
+    public static String normalize(Context context, String input) throws IllegalArgumentException, NumberParseException {
+        final Phonenumber.PhoneNumber number = getInstance(context).parse(input, getUserCountry(context));
+        if (!getInstance(context).isValidNumber(number)) {
+            throw new IllegalArgumentException("Not a valid phone number");
+        }
+        return normalize(context, number);
     }
 
     public static String normalize(Context context, Phonenumber.PhoneNumber phoneNumber) {