M build.gradle => build.gradle +1 -1
@@ 90,7 90,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:4.9.3"
implementation 'com.google.guava:guava:30.1.1-android'
- quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.36'
+ implementation 'io.michaelrocks:libphonenumber-android:8.12.36'
implementation urlFile('https://cloudflare-ipfs.com/ipfs/QmeqMiLxHi8AAjXobxr3QTfa1bSSLyAu86YviAqQnjxCjM/libwebrtc.aar', 'libwebrtc.aar')
// INSERT
}
A src/cheogram/java/eu/siacs/conversations/android/PhoneNumberContact.java => src/cheogram/java/eu/siacs/conversations/android/PhoneNumberContact.java +90 -0
@@ 0,0 1,90 @@
+package eu.siacs.conversations.android;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+
+public class PhoneNumberContact extends AbstractPhoneContact {
+
+ private final 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 ImmutableMap<String, PhoneNumberContact> load(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ return ImmutableMap.of();
+ }
+ 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 HashMap<String, PhoneNumberContact> contacts = new HashMap<>();
+ try (final Cursor cursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PROJECTION, null, null, null)){
+ 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 (final IllegalArgumentException ignored) {
+
+ }
+ }
+ } catch (final Exception e) {
+ return ImmutableMap.of();
+ }
+ return ImmutableMap.copyOf(contacts);
+ }
+
+ public static PhoneNumberContact findByUriOrNumber(Collection<PhoneNumberContact> haystack, Uri uri, String number) {
+ final PhoneNumberContact byUri = findByUri(haystack, uri);
+ return byUri != null || number == null ? byUri : findByNumber(haystack, number);
+ }
+
+ public static PhoneNumberContact findByUri(Collection<PhoneNumberContact> haystack, Uri needle) {
+ for (PhoneNumberContact contact : haystack) {
+ if (needle.equals(contact.getLookupUri())) {
+ return contact;
+ }
+ }
+ return null;
+ }
+
+ private static PhoneNumberContact findByNumber(Collection<PhoneNumberContact> haystack, String needle) {
+ for (PhoneNumberContact contact : haystack) {
+ if (needle.equals(contact.getPhoneNumber())) {
+ return contact;
+ }
+ }
+ return null;
+ }
+}
M src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java => src/cheogram/java/eu/siacs/conversations/services/QuickConversationsService.java +139 -4
@@ 1,19 1,39 @@
package eu.siacs.conversations.services;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
import android.content.Intent;
+import android.os.SystemClock;
+import android.net.Uri;
import android.util.Log;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.PhoneNumberContact;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
public class QuickConversationsService extends AbstractQuickConversationsService {
+ protected final AtomicInteger mRunningSyncJobs = new AtomicInteger(0);
+ protected final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
+ protected Attempt mLastSyncAttempt = Attempt.NULL;
+
QuickConversationsService(XmppConnectionService xmppConnectionService) {
super(xmppConnectionService);
}
@Override
public void considerSync() {
-
+ considerSync(false);
}
@Override
@@ 23,16 43,131 @@ public class QuickConversationsService extends AbstractQuickConversationsService
@Override
public boolean isSynchronizing() {
- return false;
+ return mRunningSyncJobs.get() > 0;
}
@Override
public void considerSyncBackground(boolean force) {
-
+ mRunningSyncJobs.incrementAndGet();
+ mSerialSingleThreadExecutor.execute(() -> {
+ considerSync(force);
+ if (mRunningSyncJobs.decrementAndGet() == 0) {
+ service.updateRosterUi();
+ }
+ });
}
@Override
public void handleSmsReceived(Intent intent) {
Log.d(Config.LOGTAG,"ignoring received SMS");
}
-}>
\ No newline at end of file
+
+ protected static String getNumber(final List<String> gateways, final Contact contact) {
+ final Jid jid = contact.getJid();
+ if (jid.getLocal() != null && ("quicksy.im".equals(jid.getDomain()) || gateways.contains(jid.getDomain()))) {
+ return jid.getLocal();
+ }
+ return null;
+ }
+
+ protected void refresh(Account account, final List<String> gateways, Collection<PhoneNumberContact> phoneNumberContacts) {
+ for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
+ final Uri uri = contact.getSystemAccount();
+ if (uri == null) {
+ continue;
+ }
+ final String number = getNumber(gateways, contact);
+ final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(phoneNumberContacts, uri, number);
+ final boolean needsCacheClean;
+ if (phoneNumberContact != null) {
+ if (!uri.equals(phoneNumberContact.getLookupUri())) {
+ Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
+ }
+ needsCacheClean = contact.setPhoneContact(phoneNumberContact);
+ } else {
+ needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
+ Log.d(Config.LOGTAG, uri.toString() + " vanished from address book");
+ }
+ if (needsCacheClean) {
+ service.getAvatarService().clear(contact);
+ }
+ }
+ }
+
+ protected void considerSync(boolean forced) {
+ final ImmutableMap<String, PhoneNumberContact> allContacts = PhoneNumberContact.load(service);
+ for (final Account account : service.getAccounts()) {
+ List<String> gateways = gateways(account);
+ refresh(account, gateways, allContacts.values());
+ if (!considerSync(account, gateways, allContacts, forced)) {
+ service.syncRoster(account);
+ }
+ }
+ }
+
+ protected List<String> gateways(final Account account) {
+ List<String> gateways = new ArrayList();
+ for (final Contact contact : account.getRoster().getContacts()) {
+ if (contact.showInRoster() && (contact.getPresences().anyIdentity("gateway", "pstn") || contact.getPresences().anyIdentity("gateway", "sms"))) {
+ gateways.add(contact.getJid().asBareJid().toString());
+ }
+ }
+ return gateways;
+ }
+
+ protected boolean considerSync(final Account account, final List<String> gateways, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
+ final int hash = contacts.keySet().hashCode();
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
+ if (!mLastSyncAttempt.retry(hash) && !forced) {
+ Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt sync");
+ return false;
+ }
+ mRunningSyncJobs.incrementAndGet();
+
+ mLastSyncAttempt = Attempt.create(hash);
+ final List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class);
+ for (Map.Entry<String, PhoneNumberContact> item : contacts.entrySet()) {
+ PhoneNumberContact phoneContact = item.getValue();
+ for(String gateway : gateways) {
+ final Jid jid = Jid.ofLocalAndDomain(phoneContact.getPhoneNumber(), gateway);
+ final Contact contact = account.getRoster().getContact(jid);
+ final boolean needsCacheClean = contact.setPhoneContact(phoneContact);
+ if (needsCacheClean) {
+ service.getAvatarService().clear(contact);
+ }
+ withSystemAccounts.remove(contact);
+ }
+ }
+ for (final Contact contact : withSystemAccounts) {
+ final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
+ if (needsCacheClean) {
+ service.getAvatarService().clear(contact);
+ }
+ }
+
+ mRunningSyncJobs.decrementAndGet();
+ service.syncRoster(account);
+ service.updateRosterUi();
+ return true;
+ }
+
+ protected static class Attempt {
+ private final long timestamp;
+ private final int hash;
+
+ private static final Attempt NULL = new Attempt(0, 0);
+
+ private Attempt(long timestamp, int hash) {
+ this.timestamp = timestamp;
+ this.hash = hash;
+ }
+
+ public static Attempt create(int hash) {
+ return new Attempt(SystemClock.elapsedRealtime(), hash);
+ }
+
+ public boolean retry(int hash) {
+ return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
+ }
+ }
+}
M src/cheogram/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java => src/cheogram/java/eu/siacs/conversations/utils/PhoneNumberUtilWrapper.java +91 -1
@@ 1,11 1,101 @@
package eu.siacs.conversations.utils;
import android.content.Context;
+import android.telephony.TelephonyManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
import eu.siacs.conversations.xmpp.Jid;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
+import io.michaelrocks.libphonenumber.android.Phonenumber;
public class PhoneNumberUtilWrapper {
+
+ private static volatile PhoneNumberUtil instance;
+
+
+ public static String getCountryForCode(String code) {
+ Locale locale = new Locale("", code);
+ return locale.getDisplayCountry();
+ }
+
public static String toFormattedPhoneNumber(Context context, Jid jid) {
- throw new AssertionError("This method is not implemented in Conversations");
+ try {
+ return getInstance(context).format(toPhoneNumber(context, jid), PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL).replace(' ','\u202F');
+ } catch (Exception e) {
+ return jid.getEscapedLocal();
+ }
+ }
+
+ public static Phonenumber.PhoneNumber toPhoneNumber(Context context, Jid jid) throws NumberParseException {
+ return getInstance(context).parse(jid.getEscapedLocal(), "de");
+ }
+
+ public static String normalize(Context context, String input) throws IllegalArgumentException, NumberParseException {
+ final Phonenumber.PhoneNumber number = getInstance(context).parse(input, LocationProvider.getUserCountry(context));
+ if (!getInstance(context).isValidNumber(number)) {
+ throw new IllegalArgumentException(String.format("%s is not a valid phone number", input));
+ }
+ return normalize(context, number);
+ }
+
+ public static String normalize(Context context, Phonenumber.PhoneNumber phoneNumber) {
+ return getInstance(context).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164);
+ }
+
+ public static PhoneNumberUtil getInstance(final Context context) {
+ PhoneNumberUtil localInstance = instance;
+ if (localInstance == null) {
+ synchronized (PhoneNumberUtilWrapper.class) {
+ localInstance = instance;
+ if (localInstance == null) {
+ instance = localInstance = PhoneNumberUtil.createInstance(context);
+ }
+
+ }
+ }
+ return localInstance;
}
+
+ public static List<Country> getCountries(final Context context) {
+ List<Country> countries = new ArrayList<>();
+ for (String region : getInstance(context).getSupportedRegions()) {
+ countries.add(new Country(region, getInstance(context).getCountryCodeForRegion(region)));
+ }
+ return countries;
+
+ }
+
+ public static class Country implements Comparable<Country> {
+ private final String name;
+ private final String region;
+ private final int code;
+
+ Country(String region, int code) {
+ this.name = getCountryForCode(region);
+ this.region = region;
+ this.code = code;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public String getCode() {
+ return '+' + String.valueOf(code);
+ }
+
+ @Override
+ public int compareTo(Country o) {
+ return name.compareTo(o.name);
+ }
+ }
+
}