Detailed changes
@@ -15,4 +15,8 @@ public class QuickConversationsService {
public static boolean isFull() {
return true;
}
+
+ public void considerSync() {
+
+ }
}
@@ -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);
+ }
+}
@@ -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());
+ }
+}
@@ -0,0 +1,8 @@
+package eu.siacs.conversations.android;
+
+import java.util.Collection;
+
+public interface OnPhoneContactsLoaded<T extends AbstractPhoneContact> {
+
+ void onPhoneContactsLoaded(Collection<T> contacts);
+}
@@ -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) {
@@ -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;
@@ -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();
+ });
}
@@ -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);
- }
- }
-
- }
}
@@ -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();
}
}
@@ -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);
@@ -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();
+ }
+ }
+ }
}
@@ -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());
+ }
+}
@@ -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);