refactored phone contact loading in preperation for sync

This commit is contained in:
Daniel Gultsch 2018-10-27 00:32:09 +02:00
parent 4df0cc3657
commit a49a5790c7
13 changed files with 296 additions and 186 deletions

View file

@ -15,4 +15,8 @@ public class QuickConversationsService {
public static boolean isFull() {
return true;
}
public void considerSync() {
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -0,0 +1,8 @@
package eu.siacs.conversations.android;
import java.util.Collection;
public interface OnPhoneContactsLoaded<T extends AbstractPhoneContact> {
void onPhoneContactsLoaded(Collection<T> contacts);
}

View file

@ -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) {

View file

@ -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;

View file

@ -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();
});
}

View file

@ -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);
}
}
}
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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();
}
}
}
}

View file

@ -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());
}
}

View file

@ -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);