~singpolyma/cheogram-android

a49a5790c79c1a9d59e1934159d0d67c326d1322 — Daniel Gultsch 5 years ago 4df0cc3
refactored phone contact loading in preperation for sync
M src/full/java/eu/siacs/conversations/services/QuickConversationsService.java => src/full/java/eu/siacs/conversations/services/QuickConversationsService.java +4 -0
@@ 15,4 15,8 @@ public class QuickConversationsService {
    public static boolean isFull() {
        return true;
    }

    public void considerSync() {

    }
}
\ No newline at end of file

A src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java => src/main/java/eu/siacs/conversations/android/AbstractPhoneContact.java +39 -0
@@ 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);
    }
}

A src/main/java/eu/siacs/conversations/android/JabberIdContact.java => src/main/java/eu/siacs/conversations/android/JabberIdContact.java +74 -0
@@ 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());
    }
}

A src/main/java/eu/siacs/conversations/android/OnPhoneContactsLoaded.java => src/main/java/eu/siacs/conversations/android/OnPhoneContactsLoaded.java +8 -0
@@ 0,0 1,8 @@
package eu.siacs.conversations.android;

import java.util.Collection;

public interface OnPhoneContactsLoaded<T extends  AbstractPhoneContact> {

    void onPhoneContactsLoaded(Collection<T> contacts);
}

M src/main/java/eu/siacs/conversations/entities/Contact.java => src/main/java/eu/siacs/conversations/entities/Contact.java +14 -18
@@ 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) {

M src/main/java/eu/siacs/conversations/services/ShortcutService.java => src/main/java/eu/siacs/conversations/services/ShortcutService.java +1 -1
@@ 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;

M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +32 -41
@@ 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();
        });
	}



M src/main/java/eu/siacs/conversations/utils/PhoneHelper.java => src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +0 -67
@@ 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);
			}
		}

	}
}

M src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java => src/main/java/eu/siacs/conversations/utils/ReplacingSerialSingleThreadExecutor.java +3 -7
@@ 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();
		}
	}

M src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java => src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java +1 -1
@@ 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);

M src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java => src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java +42 -51
@@ 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();
            }
        }
    }
}
\ No newline at end of file

A src/quick/java/eu/siacs/conversations/android/PhoneNumberContact.java => src/quick/java/eu/siacs/conversations/android/PhoneNumberContact.java +69 -0
@@ 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());
    }
}

M src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java => src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java +9 -0
@@ 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);