QuickConversationsService.java

  1package eu.siacs.conversations.services;
  2
  3import java.util.concurrent.ConcurrentHashMap;
  4import java.util.concurrent.atomic.AtomicInteger;
  5import java.util.Collection;
  6import java.util.Collections;
  7import java.util.Set;
  8import java.util.Objects;
  9import java.util.ArrayList;
 10import java.util.List;
 11import java.util.Map;
 12import java.util.stream.Collectors;
 13import java.util.stream.Stream;
 14
 15import com.google.common.collect.ImmutableMap;
 16import com.google.common.collect.ImmutableList;
 17
 18import android.content.Intent;
 19import android.os.Binder;
 20import android.os.SystemClock;
 21import android.net.Uri;
 22import android.util.Log;
 23
 24import eu.siacs.conversations.Config;
 25import eu.siacs.conversations.android.PhoneNumberContact;
 26import eu.siacs.conversations.entities.Account;
 27import eu.siacs.conversations.entities.Contact;
 28import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 29import eu.siacs.conversations.xmpp.Jid;
 30import eu.siacs.conversations.xmpp.manager.RosterManager;
 31
 32public class QuickConversationsService extends AbstractQuickConversationsService {
 33
 34	public class QuickConversationsBinder extends Binder {
 35		public QuickConversationsService getService() {
 36			return QuickConversationsService.this;
 37		}
 38	}
 39
 40    protected final AtomicInteger mRunningSyncJobs = new AtomicInteger(0);
 41    protected final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName());
 42    protected Map<String, Attempt> mLastSyncAttempt = new ConcurrentHashMap<>();
 43
 44    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 45        super(xmppConnectionService);
 46    }
 47
 48    @Override
 49    public void considerSync() {
 50        considerSync(false);
 51    }
 52
 53    @Override
 54    public void signalAccountStateChange() {
 55
 56    }
 57
 58    @Override
 59    public boolean isSynchronizing() {
 60        return mRunningSyncJobs.get() > 0;
 61    }
 62
 63    @Override
 64    public void considerSyncBackground(boolean force) {
 65        mRunningSyncJobs.incrementAndGet();
 66        mSerialSingleThreadExecutor.execute(() -> {
 67            considerSync(force);
 68            if (mRunningSyncJobs.decrementAndGet() == 0) {
 69                service.updateRosterUi(XmppConnectionService.UpdateRosterReason.INIT);
 70            }
 71        });
 72    }
 73
 74    @Override
 75    public void handleSmsReceived(Intent intent) {
 76        Log.d(Config.LOGTAG,"ignoring received SMS");
 77    }
 78
 79    protected static String getNumber(final Set<String> gateways, final Contact contact) {
 80        final Jid jid = contact.getJid();
 81        if (jid.getLocal() != null && ("quicksy.im".equals(jid.getDomain()) || gateways.contains(jid.getDomain()))) {
 82            return jid.getLocal();
 83        }
 84        return null;
 85    }
 86
 87    protected void refresh(Account account, final Set<String> gateways, Collection<PhoneNumberContact> phoneNumberContacts) {
 88        for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
 89            final Uri uri = contact.getSystemAccount();
 90            if (uri == null) {
 91                continue;
 92            }
 93            final String number = getNumber(gateways, contact);
 94            final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(phoneNumberContacts, uri, number);
 95            final boolean needsCacheClean;
 96            if (phoneNumberContact != null) {
 97                if (!uri.equals(phoneNumberContact.getLookupUri())) {
 98                    Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri());
 99                }
100                needsCacheClean = contact.setPhoneContact(phoneNumberContact);
101            } else {
102                needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
103                Log.d(Config.LOGTAG, uri.toString() + " vanished from address book");
104            }
105            if (needsCacheClean) {
106                service.getAvatarService().clear(contact);
107            }
108        }
109    }
110
111    protected void considerSync(boolean forced) {
112        ImmutableMap<String, PhoneNumberContact> allContacts = null;
113        for (final Account account : ImmutableList.copyOf(service.getAccounts())) {
114            final var gateways = gateways(account);
115            if (allContacts == null) allContacts = PhoneNumberContact.load(service);
116            refresh(account, gateways, allContacts.values());
117            if (!considerSync(account, gateways, allContacts, forced)) {
118                account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
119            }
120        }
121    }
122
123    protected Set<String> gateways(final Account account) {
124        return Stream.concat(
125            account.getGateways("pstn").stream(),
126            account.getGateways("sms").stream()
127        ).map(a -> a.getJid().asBareJid().toString()).collect(Collectors.toSet());
128    }
129
130    protected boolean considerSync(final Account account, final Set<String> gateways, final Map<String, PhoneNumberContact> contacts, final boolean forced) {
131        final int hash = Objects.hash(contacts.keySet(), gateways);
132        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash);
133        if (!mLastSyncAttempt.getOrDefault(account.getUuid(), Attempt.NULL).retry(hash) && !forced) {
134            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt sync");
135            return false;
136        }
137        mRunningSyncJobs.incrementAndGet();
138
139        mLastSyncAttempt.put(account.getUuid(), Attempt.create(hash));
140        final List<Contact> withSystemAccounts = new ArrayList<>(account.getRoster().getWithSystemAccounts(PhoneNumberContact.class));
141        for (Map.Entry<String, PhoneNumberContact> item : contacts.entrySet()) {
142            PhoneNumberContact phoneContact = item.getValue();
143            for(String gateway : gateways) {
144                final Jid jid = Jid.ofLocalAndDomain(phoneContact.getPhoneNumber(), gateway);
145                final Contact contact = account.getRoster().getContact(jid);
146                boolean needsCacheClean = contact.setPhoneContact(phoneContact);
147                needsCacheClean |= contact.setSystemTags(phoneContact.getTags());
148                if (needsCacheClean) {
149                    service.getAvatarService().clear(contact);
150                }
151                withSystemAccounts.remove(contact);
152            }
153        }
154        for (final Contact contact : withSystemAccounts) {
155            final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class);
156            if (needsCacheClean) {
157                service.getAvatarService().clear(contact);
158            }
159        }
160
161        mRunningSyncJobs.decrementAndGet();
162        account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
163        service.updateRosterUi(XmppConnectionService.UpdateRosterReason.INIT);
164        return true;
165    }
166
167    protected static class Attempt {
168        private final long timestamp;
169        private final int hash;
170
171        private static final Attempt NULL = new Attempt(0, 0);
172
173        private Attempt(long timestamp, int hash) {
174            this.timestamp = timestamp;
175            this.hash = hash;
176        }
177
178        public static Attempt create(int hash) {
179            return new Attempt(SystemClock.elapsedRealtime(), hash);
180        }
181
182        public boolean retry(int hash) {
183            return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL;
184        }
185    }
186}