Merge branch 'sync-tel-contacts'
* sync-tel-contacts: Sync system contacts by phone number
This commit is contained in:
commit
f3fb32af66
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
public static String toFormattedPhoneNumber(Context context, Jid jid) {
|
||||
throw new AssertionError("This method is not implemented in Conversations");
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue