refactored phone contact loading in preperation for sync

Daniel Gultsch created

Change summary

src/full/java/eu/siacs/conversations/services/QuickConversationsService.java        |  4 
src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java              | 39 
src/main/java/eu/siacs/conversations/android/JabberIdContact.java                   | 74 
src/main/java/eu/siacs/conversations/android/OnPhoneContactsLoaded.java             |  8 
src/main/java/eu/siacs/conversations/entities/Contact.java                          | 32 
src/main/java/eu/siacs/conversations/services/ShortcutService.java                  |  2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java            | 73 
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                         | 67 
src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java | 10 
src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java                |  2 
src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java          | 93 
src/quick/java/eu/siacs/conversations/android/PhoneNumberContact.java               | 69 
src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java       |  9 
13 files changed, 296 insertions(+), 186 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java 🔗

@@ -0,0 +1,39 @@
+package eu.siacs.conversations.android;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+
+abstract class AbstractPhoneContact {
+
+    private final Uri lookupUri;
+    private final String displayName;
+    private final String photoUri;
+
+
+    AbstractPhoneContact(Cursor cursor) {
+        int phoneId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data._ID));
+        String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY));
+        this.lookupUri = ContactsContract.Contacts.getLookupUri(phoneId, lookupKey);
+        this.displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
+        this.photoUri = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.PHOTO_URI));
+    }
+
+    public Uri getLookupUri() {
+        return lookupUri;
+    }
+
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public String getPhotoUri() {
+        return photoUri;
+    }
+
+
+    public int rating() {
+        return (TextUtils.isEmpty(displayName) ? 0 : 2) + (TextUtils.isEmpty(photoUri) ? 0 : 1);
+    }
+}

src/main/java/eu/siacs/conversations/android/JabberIdContact.java 🔗

@@ -0,0 +1,74 @@
+package eu.siacs.conversations.android;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+import eu.siacs.conversations.Config;
+import rocks.xmpp.addr.Jid;
+
+public class JabberIdContact extends AbstractPhoneContact {
+
+    private final Jid jid;
+
+    private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
+        super(cursor);
+        try {
+            this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
+        } catch (IllegalArgumentException | NullPointerException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public Jid getJid() {
+        return jid;
+    }
+
+    public static void load(Context context, OnPhoneContactsLoaded<JabberIdContact> callback) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+            callback.onPhoneContactsLoaded(Collections.emptyList());
+            return;
+        }
+        final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
+                ContactsContract.Data.DISPLAY_NAME,
+                ContactsContract.Data.PHOTO_URI,
+                ContactsContract.Data.LOOKUP_KEY,
+                ContactsContract.CommonDataKinds.Im.DATA};
+
+        final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+                + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+                + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+                + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+                + "\")";
+        final Cursor cursor;
+        try {
+            cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, null);
+        } catch (Exception e) {
+            callback.onPhoneContactsLoaded(Collections.emptyList());
+            return;
+        }
+        final HashMap<Jid, JabberIdContact> contacts = new HashMap<>();
+        while (cursor != null && cursor.moveToNext()) {
+            try {
+                final JabberIdContact contact = new JabberIdContact(cursor);
+                final JabberIdContact preexisting = contacts.put(contact.getJid(), contact);
+                if (preexisting == null || preexisting.rating() < contact.rating()) {
+                    contacts.put(contact.getJid(), contact);
+                }
+            } catch (IllegalArgumentException e) {
+                Log.d(Config.LOGTAG,"unable to create jabber id contact");
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+        callback.onPhoneContactsLoaded(contacts.values());
+    }
+}

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

@@ -48,7 +48,7 @@ public class Contact implements ListItem, Blockable {
 	private String commonName;
 	protected Jid jid;
 	private int subscription = 0;
-	private String systemAccount;
+	private Uri systemAccount;
 	private String photoUri;
 	private final JSONObject keys;
 	private JSONArray groups = new JSONArray();
@@ -62,7 +62,7 @@ public class Contact implements ListItem, Blockable {
 
 	public Contact(final String account, final String systemName, final String serverName,
 	               final Jid jid, final int subscription, final String photoUri,
-	               final String systemAccount, final String keys, final String avatar, final long lastseen,
+	               final Uri systemAccount, final String keys, final String avatar, final long lastseen,
 	               final String presence, final String groups) {
 		this.accountUuid = account;
 		this.systemName = systemName;
@@ -105,13 +105,19 @@ public class Contact implements ListItem, Blockable {
 			// TODO: Borked DB... handle this somehow?
 			return null;
 		}
+		Uri systemAccount;
+		try {
+			systemAccount = Uri.parse(cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)));
+		} catch (Exception e) {
+			systemAccount = null;
+		}
 		return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 				cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
 				cursor.getString(cursor.getColumnIndex(SERVERNAME)),
 				jid,
 				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
 				cursor.getString(cursor.getColumnIndex(PHOTOURI)),
-				cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+				systemAccount,
 				cursor.getString(cursor.getColumnIndex(KEYS)),
 				cursor.getString(cursor.getColumnIndex(AVATAR)),
 				cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
@@ -200,7 +206,7 @@ public class Contact implements ListItem, Blockable {
 			values.put(SERVERNAME, serverName);
 			values.put(JID, jid.toString());
 			values.put(OPTIONS, subscription);
-			values.put(SYSTEMACCOUNT, systemAccount);
+			values.put(SYSTEMACCOUNT, systemAccount != null ? systemAccount.toString() : null);
 			values.put(PHOTOURI, photoUri);
 			values.put(KEYS, keys.toString());
 			values.put(AVATAR, avatar == null ? null : avatar.getFilename());
@@ -270,21 +276,11 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	public Uri getSystemAccount() {
-		if (systemAccount == null) {
-			return null;
-		} else {
-			String[] parts = systemAccount.split("#");
-			if (parts.length != 2) {
-				return null;
-			} else {
-				long id = Long.parseLong(parts[0]);
-				return ContactsContract.Contacts.getLookupUri(id, parts[1]);
-			}
-		}
+		return systemAccount;
 	}
 
-	public void setSystemAccount(String account) {
-		this.systemAccount = account;
+	public void setSystemAccount(Uri lookupUri) {
+		this.systemAccount = lookupUri;
 	}
 
 	private Collection<String> getGroups(final boolean unique) {
@@ -343,7 +339,7 @@ public class Contact implements ListItem, Blockable {
 	}
 
 	public boolean showInPhoneBook() {
-		return systemAccount != null && !systemAccount.trim().isEmpty();
+		return systemAccount != null;
 	}
 
 	public void parseSubscriptionFromElement(Element item) {

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

@@ -25,7 +25,7 @@ import rocks.xmpp.addr.Jid;
 public class ShortcutService {
 
     private final XmppConnectionService xmppConnectionService;
-    private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(false);
+    private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
 
     public ShortcutService(XmppConnectionService xmppConnectionService) {
         this.xmppConnectionService = xmppConnectionService;

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

@@ -71,6 +71,7 @@ import java.util.concurrent.atomic.AtomicLong;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
+import eu.siacs.conversations.android.JabberIdContact;
 import eu.siacs.conversations.crypto.OmemoSetting;
 import eu.siacs.conversations.crypto.PgpDecryptionService;
 import eu.siacs.conversations.crypto.PgpEngine;
@@ -115,7 +116,6 @@ import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.ExceptionHelper;
 import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
 import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.QuickLoader;
 import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
@@ -192,7 +192,7 @@ public class XmppConnectionService extends Service {
         }
     };
     public DatabaseBackend databaseBackend;
-    private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true);
+    private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
     private long mLastActivity = 0;
     private FileBackend fileBackend = new FileBackend(this);
     private MemorizingTrustManager mMemorizingTrustManager;
@@ -1519,45 +1519,36 @@ public class XmppConnectionService extends Service {
 	}
 
 	public void loadPhoneContacts() {
-		mContactMergerExecutor.execute(() -> PhoneHelper.loadPhoneContacts(XmppConnectionService.this, new OnPhoneContactsLoadedListener() {
-			@Override
-			public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
-				Log.d(Config.LOGTAG, "start merging phone contacts with roster");
-				for (Account account : accounts) {
-					List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
-					for (Bundle phoneContact : phoneContacts) {
-						Jid jid;
-						try {
-							jid = Jid.of(phoneContact.getString("jid"));
-						} catch (final IllegalArgumentException e) {
-							continue;
-						}
-						final Contact contact = account.getRoster().getContact(jid);
-						String systemAccount = phoneContact.getInt("phoneid")
-								+ "#"
-								+ phoneContact.getString("lookup");
-						contact.setSystemAccount(systemAccount);
-						boolean needsCacheClean = contact.setPhotoUri(phoneContact.getString("photouri"));
-						needsCacheClean |= contact.setSystemName(phoneContact.getString("displayname"));
-						if (needsCacheClean) {
-							getAvatarService().clear(contact);
-						}
-						withSystemAccounts.remove(contact);
-					}
-					for (Contact contact : withSystemAccounts) {
-						contact.setSystemAccount(null);
-						boolean needsCacheClean = contact.setPhotoUri(null);
-						needsCacheClean |= contact.setSystemName(null);
-						if (needsCacheClean) {
-							getAvatarService().clear(contact);
-						}
-					}
-				}
-				Log.d(Config.LOGTAG, "finished merging phone contacts");
-				mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true));
-				updateRosterUi();
-			}
-		}));
+        mContactMergerExecutor.execute(() -> {
+            JabberIdContact.load(this, contacts -> {
+                Log.d(Config.LOGTAG, "start merging phone contacts with roster");
+                for (Account account : accounts) {
+                    List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
+                    for (JabberIdContact jidContact : contacts) {
+                        final Contact contact = account.getRoster().getContact(jidContact.getJid());
+                        contact.setSystemAccount(jidContact.getLookupUri());
+                        boolean needsCacheClean = contact.setPhotoUri(jidContact.getPhotoUri());
+                        needsCacheClean |= contact.setSystemName(jidContact.getDisplayName());
+                        if (needsCacheClean) {
+                            getAvatarService().clear(contact);
+                        }
+                        withSystemAccounts.remove(contact);
+                    }
+                    for (Contact contact : withSystemAccounts) {
+                        contact.setSystemAccount(null);
+                        boolean needsCacheClean = contact.setPhotoUri(null);
+                        needsCacheClean |= contact.setSystemName(null);
+                        if (needsCacheClean) {
+                            getAvatarService().clear(contact);
+                        }
+                    }
+                }
+                Log.d(Config.LOGTAG, "finished merging phone contacts");
+                mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true));
+                updateRosterUi();
+            });
+            mQuickConversationsService.considerSync();
+        });
 	}
 
 

src/main/java/eu/siacs/conversations/utils/PhoneHelper.java 🔗

@@ -24,55 +24,6 @@ public class PhoneHelper {
 		return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
 	}
 
-	public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) {
-		final List<Bundle> phoneContacts = new ArrayList<>();
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-				&& context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
-			listener.onPhoneContactsLoaded(phoneContacts);
-			return;
-		}
-		final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
-				ContactsContract.Data.DISPLAY_NAME,
-				ContactsContract.Data.PHOTO_URI,
-				ContactsContract.Data.LOOKUP_KEY,
-				ContactsContract.CommonDataKinds.Im.DATA};
-
-		final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
-				+ ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
-				+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
-				+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
-				+ "\")";
-
-		CursorLoader mCursorLoader = new NotThrowCursorLoader(context,
-				ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
-				null);
-		mCursorLoader.registerListener(0, (arg0, c) -> {
-			if (c != null) {
-				while (c.moveToNext()) {
-					Bundle contact = new Bundle();
-					contact.putInt("phoneid", c.getInt(c.getColumnIndex(ContactsContract.Data._ID)));
-					contact.putString("displayname", c.getString(c.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
-					contact.putString("photouri", c.getString(c.getColumnIndex(ContactsContract.Data.PHOTO_URI)));
-					contact.putString("lookup", c.getString(c.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
-					contact.putString("jid", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
-					phoneContacts.add(contact);
-				}
-				c.close();
-			}
-
-			if (listener != null) {
-				listener.onPhoneContactsLoaded(phoneContacts);
-			}
-		});
-		try {
-			mCursorLoader.startLoading();
-		} catch (RejectedExecutionException e) {
-			if (listener != null) {
-				listener.onPhoneContactsLoaded(phoneContacts);
-			}
-		}
-	}
-
 	public static Uri getProfilePictureUri(Context context) {
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
 			return null;
@@ -104,22 +55,4 @@ public class PhoneHelper {
 			return "unknown";
 		}
 	}
-
-	private static class NotThrowCursorLoader extends CursorLoader {
-
-		private NotThrowCursorLoader(Context c, Uri u, String[] p, String s, String[] sa, String so) {
-			super(c, u, p, s, sa, so);
-		}
-
-		@Override
-		public Cursor loadInBackground() {
-
-			try {
-				return (super.loadInBackground());
-			} catch (Throwable e) {
-				return (null);
-			}
-		}
-
-	}
 }

src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java 🔗

@@ -3,17 +3,13 @@ package eu.siacs.conversations.utils;
 public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecutor {
 
 	public ReplacingSerialSingleThreadExecutor(String name) {
-		super(name, false);
-	}
-
-	public ReplacingSerialSingleThreadExecutor(boolean prepareLooper) {
-		super(ReplacingSerialSingleThreadExecutor.class.getName(), prepareLooper);
+		super(name);
 	}
 
 	@Override
 	public synchronized void execute(final Runnable r) {
 		tasks.clear();
-		if (active != null && active instanceof Cancellable) {
+		if (active instanceof Cancellable) {
 			((Cancellable) active).cancel();
 		}
 		super.execute(r);
@@ -21,7 +17,7 @@ public class ReplacingSerialSingleThreadExecutor extends SerialSingleThreadExecu
 
 	public synchronized void cancelRunningTasks() {
 		tasks.clear();
-		if (active != null && active instanceof Cancellable) {
+		if (active instanceof Cancellable) {
 			((Cancellable) active).cancel();
 		}
 	}

src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java 🔗

@@ -42,7 +42,7 @@ public class ReplacingTaskManager {
 		synchronized (this.executors) {
 			executor = this.executors.get(account);
 			if (executor == null) {
-				executor = new ReplacingSerialSingleThreadExecutor(false);
+				executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName());
 				this.executors.put(account, executor);
 			}
 			executor.execute(runnable);

src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java 🔗

@@ -1,73 +1,64 @@
 package eu.siacs.conversations.utils;
 
-import android.os.Looper;
 import android.util.Log;
 
 import java.util.ArrayDeque;
-import java.util.Queue;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 import eu.siacs.conversations.Config;
-import eu.siacs.conversations.services.AttachFileToConversationRunnable;
 
 public class SerialSingleThreadExecutor implements Executor {
 
-	private final Executor executor = Executors.newSingleThreadExecutor();
-	final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
-	protected Runnable active;
-	private final String name;
+    final ArrayDeque<Runnable> tasks = new ArrayDeque<>();
+    private final Executor executor = Executors.newSingleThreadExecutor();
+    private final String name;
+    protected Runnable active;
 
-	public SerialSingleThreadExecutor(String name) {
-		this(name, false);
-	}
 
-	SerialSingleThreadExecutor(String name, boolean prepareLooper) {
-		if (prepareLooper) {
-			execute(Looper::prepare);
-		}
-		this.name = name;
-	}
+    public SerialSingleThreadExecutor(String name) {
+        this.name = name;
+    }
 
-	public synchronized void execute(final Runnable r) {
-		tasks.offer(new Runner(r));
-		if (active == null) {
-			scheduleNext();
-		}
-	}
+    public synchronized void execute(final Runnable r) {
+        tasks.offer(new Runner(r));
+        if (active == null) {
+            scheduleNext();
+        }
+    }
 
-	private synchronized void scheduleNext() {
-		if ((active = tasks.poll()) != null) {
-			executor.execute(active);
-			int remaining = tasks.size();
-			if (remaining > 0) {
-				Log.d(Config.LOGTAG,remaining+" remaining tasks on executor '"+name+"'");
-			}
-		}
-	}
+    private synchronized void scheduleNext() {
+        if ((active = tasks.poll()) != null) {
+            executor.execute(active);
+            int remaining = tasks.size();
+            if (remaining > 0) {
+                Log.d(Config.LOGTAG, remaining + " remaining tasks on executor '" + name + "'");
+            }
+        }
+    }
 
-	private class Runner implements Runnable, Cancellable {
+    private class Runner implements Runnable, Cancellable {
 
-		private final Runnable runnable;
+        private final Runnable runnable;
 
-		private Runner(Runnable runnable) {
-			this.runnable = runnable;
-		}
+        private Runner(Runnable runnable) {
+            this.runnable = runnable;
+        }
 
-		@Override
-		public void cancel() {
-			if (runnable instanceof Cancellable) {
-				((Cancellable) runnable).cancel();
-			}
-		}
+        @Override
+        public void cancel() {
+            if (runnable instanceof Cancellable) {
+                ((Cancellable) runnable).cancel();
+            }
+        }
 
-		@Override
-		public void run() {
-			try {
-				runnable.run();
-			} finally {
-				scheduleNext();
-			}
-		}
-	}
+        @Override
+        public void run() {
+            try {
+                runnable.run();
+            } finally {
+                scheduleNext();
+            }
+        }
+    }
 }

src/quick/java/eu/siacs/conversations/android/PhoneNumberContact.java 🔗

@@ -0,0 +1,69 @@
+package eu.siacs.conversations.android;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+
+public class PhoneNumberContact extends AbstractPhoneContact {
+
+    private String phoneNumber;
+
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    private PhoneNumberContact(Context context, Cursor cursor) throws IllegalArgumentException {
+        super(cursor);
+        try {
+            this.phoneNumber = PhoneNumberUtilWrapper.normalize(context,cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
+        } catch (NumberParseException | NullPointerException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    public static void load(Context context, OnPhoneContactsLoaded<PhoneNumberContact> callback) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+            callback.onPhoneContactsLoaded(Collections.emptyList());
+            return;
+        }
+        final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
+                ContactsContract.Data.DISPLAY_NAME,
+                ContactsContract.Data.PHOTO_URI,
+                ContactsContract.Data.LOOKUP_KEY,
+                ContactsContract.CommonDataKinds.Phone.NUMBER};
+        final Cursor cursor;
+        try {
+            cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null);
+        } catch (Exception e) {
+            callback.onPhoneContactsLoaded(Collections.emptyList());
+            return;
+        }
+        final HashMap<String, PhoneNumberContact> contacts = new HashMap<>();
+        while (cursor != null && cursor.moveToNext()) {
+            try {
+                final PhoneNumberContact contact = new PhoneNumberContact(context, cursor);
+                final PhoneNumberContact preexisting = contacts.get(contact.getPhoneNumber());
+                if (preexisting == null || preexisting.rating() < contact.rating()) {
+                    contacts.put(contact.getPhoneNumber(), contact);
+                }
+            } catch (IllegalArgumentException e) {
+                Log.d(Config.LOGTAG, "unable to create phone contact");
+            }
+        }
+        if (cursor != null) {
+            cursor.close();
+        }
+        callback.onPhoneContactsLoaded(contacts.values());
+    }
+}

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

@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.net.ssl.SSLHandshakeException;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
 import eu.siacs.conversations.crypto.sasl.Plain;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.AccountUtils;
@@ -264,6 +265,14 @@ public class QuickConversationsService {
         return false;
     }
 
+    public void considerSync() {
+        PhoneNumberContact.load(service, contacts -> {
+            for(PhoneNumberContact c : contacts) {
+                Log.d(Config.LOGTAG, "Display Name=" + c.getDisplayName() + ", number=" +  c.getPhoneNumber()+", uri="+c.getLookupUri());
+            }
+        });
+    }
+
     public interface OnVerificationRequested {
         void onVerificationRequestFailed(int code);